feat: fix systemd user and add static-web role

This commit is contained in:
Clément Désiles 2025-12-20 23:14:00 +01:00
parent 787c171f65
commit ba94509bca
No known key found for this signature in database
11 changed files with 301 additions and 11 deletions

View File

@ -3,19 +3,23 @@
# Transparent TCP proxy (no protocol inspection)
{% if config.http | default(true) %}
upstream {{ domain | replace('.', '_') | replace('-', '_') }}_http {
server {{ config.forward_to }}:80;
}
server {
listen 80;
# Using variable forces runtime DNS resolution (avoids startup failures)
set $upstream_http {{ config.forward_to }};
proxy_pass $upstream_http:80;
proxy_pass {{ domain | replace('.', '_') | replace('-', '_') }}_http;
}
{% endif %}
{% if config.https | default(true) %}
upstream {{ domain | replace('.', '_') | replace('-', '_') }}_https {
server {{ config.forward_to }}:443;
}
server {
listen 443;
# Using variable forces runtime DNS resolution (avoids startup failures)
set $upstream_https {{ config.forward_to }};
proxy_pass $upstream_https:443;
proxy_pass {{ domain | replace('.', '_') | replace('-', '_') }}_https;
}
{% endif %}

View File

@ -57,11 +57,6 @@ http {
{% if nginx_forwarder and nginx_forwarder | length > 0 %}
# Stream block for TCP/UDP proxying
stream {
# DNS resolver for runtime hostname resolution
# Using 127.0.0.1 (systemd-resolved) with 30s cache and 5s timeout
resolver 127.0.0.1 valid=30s ipv6=off;
resolver_timeout 5s;
# Load stream configurations
include {{ nginx_streams_dir }}/*.conf;
}

View File

@ -6,6 +6,8 @@ After=network-online.target
[Service]
Type=oneshot
RemainAfterExit=true
User={{ ansible_user }}
Group={{ ansible_user }}
WorkingDirectory={{ podman_projects_dir }}/ntfy
ExecStart=/usr/bin/podman-compose up -d
ExecStop=/usr/bin/podman-compose down

110
roles/static-web/README.md Normal file
View File

@ -0,0 +1,110 @@
# static-web
Deploy static websites from Git repositories with Nginx.
## Features
- Clone static sites from Git repositories
- Automatic Nginx vhost configuration
- HTTPS enabled by default with Let's Encrypt
- Support for build commands (npm, hugo, jekyll, etc.)
- Subdirectory serving (for built assets)
- Static file caching
- Security headers (including HSTS for HTTPS)
## Dependencies
- nginx role (automatically included via meta/main.yml)
## Variables
See [defaults/main.yml](defaults/main.yml)
**Main configuration:**
```yaml
static_web_sites:
"portfolio.example.fr":
git_repo: "https://github.com/example/portfolio.git"
git_branch: "main" # Optional, defaults to main
git_depth: 1 # Optional, shallow clone
build_command: "npm install && npm run build" # Optional
root_dir: "dist" # Optional, serve subdirectory
ssl_enabled: true # Optional, defaults to true (HTTPS)
"blog.example.com":
git_repo: "https://github.com/example/blog.git"
# ssl_enabled defaults to true, set to false for HTTP only
```
## Usage
**Inventory (host_vars or group_vars):**
```yaml
static_web_sites:
"portfolio.example.fr":
git_repo: "https://github.com/username/portfolio.git"
"docs.example.com":
git_repo: "https://github.com/company/documentation.git"
git_branch: "gh-pages"
root_dir: "_site"
```
**Playbook:**
```yaml
- hosts: webservers
roles:
- static-web
```
## File Structure
Sites are deployed to `/var/www/static/<hostname>/`
Example:
```
/var/www/static/
├── portfolio.example.fr/
│ └── index.html
└── blog.example.com/
├── _site/ # Built assets (if root_dir specified)
└── ...
```
## Advanced Examples
**Hugo site:**
```yaml
static_web_sites:
"blog.example.com":
git_repo: "https://github.com/example/hugo-blog.git"
build_command: "hugo --minify"
root_dir: "public"
```
**React app:**
```yaml
static_web_sites:
"app.example.com":
git_repo: "https://github.com/example/react-app.git"
build_command: "npm ci && npm run build"
root_dir: "build"
```
## Updating Sites
Re-run the playbook to pull latest changes:
```bash
ansible-playbook -i inventory playbook.yml --tags static-web
```
## Notes
- Nginx configuration is deployed to `{{ nginx_conf_dir }}/<hostname>.conf`
- Sites are owned by nginx user (www-data on Debian, http on Arch)
- Git clones use shallow clone (depth=1) by default for efficiency
- Build commands run as nginx user

View File

@ -0,0 +1,20 @@
---
# Static web sites configuration
# Define sites as a dictionary with hostname as key
# Example:
# static_web_sites:
# "portfolio.example.fr":
# git_repo: "https://github.com/example/portfolio.git"
# git_branch: "main" # optional, defaults to main
# git_depth: 1 # optional, shallow clone depth
# build_command: "" # optional, command to run after git clone (e.g., npm build)
# root_dir: "" # optional, subdirectory to serve (e.g., "dist" or "build")
# ssl_enabled: true # optional, enable HTTPS with Let's Encrypt (default: true)
static_web_sites: {}
# Base directory for static web sites
static_web_base_dir: /var/www/static
# Nginx user (auto-detected from nginx role)
# static_web_nginx_user: www-data # Set by nginx role vars

View File

@ -0,0 +1,5 @@
---
- name: Reload nginx
ansible.builtin.systemd:
name: nginx
state: reloaded

View File

@ -0,0 +1,3 @@
---
dependencies:
- role: nginx

View File

@ -0,0 +1,67 @@
---
- name: Load OS-specific variables for nginx
ansible.builtin.include_vars: "{{ item }}"
with_first_found:
- "../../nginx/vars/{{ ansible_facts['os_family'] }}.yml"
- "../../nginx/vars/debian.yml"
- name: Install git
ansible.builtin.package:
name: git
state: present
- name: Ensure static web base directory exists
ansible.builtin.file:
path: "{{ static_web_base_dir }}"
state: directory
owner: root
group: root
mode: "0755"
- name: Create site directories
ansible.builtin.file:
path: "{{ static_web_base_dir }}/{{ item.key }}"
state: directory
owner: "{{ nginx_user }}"
group: "{{ nginx_user }}"
mode: "0755"
loop: "{{ static_web_sites | dict2items }}"
when: static_web_sites | length > 0
- name: Clone or update git repositories
ansible.builtin.git:
repo: "{{ item.value.git_repo }}"
dest: "{{ static_web_base_dir }}/{{ item.key }}"
version: "{{ item.value.git_branch | default('main') }}"
depth: "{{ item.value.git_depth | default(1) }}"
force: true
loop: "{{ static_web_sites | dict2items }}"
when: static_web_sites | length > 0
become_user: "{{ nginx_user }}"
notify: Reload nginx
- name: Run build commands if specified
ansible.builtin.shell: "{{ item.value.build_command }}"
args:
chdir: "{{ static_web_base_dir }}/{{ item.key }}"
loop: "{{ static_web_sites | dict2items }}"
when:
- static_web_sites | length > 0
- item.value.build_command is defined
- item.value.build_command | length > 0
become_user: "{{ nginx_user }}"
changed_when: true
- name: Deploy nginx vhost configurations
ansible.builtin.template:
src: nginx-vhost.conf.j2
dest: "{{ nginx_conf_dir }}/{{ item.key }}.conf"
owner: root
group: root
mode: "0644"
loop: "{{ static_web_sites | dict2items }}"
vars:
hostname: "{{ item.key }}"
site_config: "{{ item.value }}"
when: static_web_sites | length > 0
notify: Reload nginx

View File

@ -0,0 +1,79 @@
# Static web vhost for {{ hostname }}
# Managed by Ansible - DO NOT EDIT MANUALLY
server {
listen 80;
listen [::]:80;
server_name {{ hostname }};
{% if site_config.ssl_enabled | default(true) %}
# Certbot webroot for ACME challenges
location /.well-known/acme-challenge/ {
root /var/www/certbot;
}
# Redirect to HTTPS
location / {
return 301 https://$server_name$request_uri;
}
}
server {
listen 443 ssl;
listen [::]:443 ssl;
server_name {{ hostname }};
# Let's Encrypt certificates (managed by Certbot)
ssl_certificate /etc/letsencrypt/live/{{ hostname }}/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/{{ hostname }}/privkey.pem;
# SSL configuration
ssl_protocols {{ nginx_ssl_protocols }};
ssl_prefer_server_ciphers {{ 'on' if nginx_ssl_prefer_server_ciphers else 'off' }};
{% endif %}
# Document root
{% if site_config.root_dir is defined and site_config.root_dir | length > 0 %}
root {{ static_web_base_dir }}/{{ hostname }}/{{ site_config.root_dir }};
{% else %}
root {{ static_web_base_dir }}/{{ hostname }};
{% endif %}
# Index files
index index.html index.htm;
# 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;
{% if site_config.ssl_enabled | default(true) %}
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
{% endif %}
# Logging
{% if nginx_log_backend == 'journald' %}
access_log syslog:server=unix:/dev/log,nohostname,tag=nginx_{{ hostname | replace('.', '_') | replace('-', '_') }};
error_log syslog:server=unix:/dev/log,nohostname,tag=nginx_{{ hostname | replace('.', '_') | replace('-', '_') }};
{% else %}
access_log /var/log/nginx/{{ hostname }}-access.log;
error_log /var/log/nginx/{{ hostname }}-error.log;
{% endif %}
# Main location
location / {
try_files $uri $uri/ =404;
}
# Deny access to hidden files
location ~ /\. {
deny all;
access_log off;
log_not_found off;
}
# Static file caching
location ~* \.(jpg|jpeg|png|gif|ico|css|js|svg|woff|woff2|ttf|eot)$ {
expires 1y;
add_header Cache-Control "public, immutable";
}
}

View File

@ -165,6 +165,9 @@
[Unit]
After=wg-quick@wg0.service
Requires=wg-quick@wg0.service
# Make Unbound part of network-online.target (provides DNS)
Before=network-online.target
Wants=network-online.target
notify: Reload systemd and restart unbound
- name: Enables unbound service

View File

@ -6,6 +6,8 @@ After=network-online.target
[Service]
Type=oneshot
RemainAfterExit=true
User={{ ansible_user }}
Group={{ ansible_user }}
WorkingDirectory={{ podman_projects_dir }}/uptime-kuma
ExecStart=/usr/bin/podman-compose up -d
ExecStop=/usr/bin/podman-compose down