netsapiensis

rocky-webstack

0
0
# Install this skill:
npx skills add netsapiensis/claude-code-skills --skill "rocky-webstack"

Install specific skill from multi-skill repository

# Description

Rocky Linux 8/9 web and application stack including Nginx, Apache httpd, PHP-FPM pool configuration, MariaDB administration, SSL/TLS with certbot, and reverse proxy patterns including WebSocket. Use when setting up web servers, PHP applications, databases, TLS certificates, or reverse proxies.

# SKILL.md


name: rocky-webstack
description: Rocky Linux 8/9 web and application stack including Nginx, Apache httpd, PHP-FPM pool configuration, MariaDB administration, SSL/TLS with certbot, and reverse proxy patterns including WebSocket. Use when setting up web servers, PHP applications, databases, TLS certificates, or reverse proxies.


Rocky Linux Web & Application Stack

Nginx, Apache, PHP-FPM, MariaDB, SSL/TLS, and reverse proxy patterns for Rocky Linux 8/9.

Prerequisite: See rocky-foundation for OS detection and safety tier definitions.

Nginx

Installation

ROCKY_VERSION=$(source /etc/os-release && echo "${VERSION_ID%%.*}")

# Rocky 8: Choose module stream  # [CONFIRM]
if [[ "$ROCKY_VERSION" == "8" ]]; then
    dnf module list nginx                    # [READ-ONLY]
    dnf module enable nginx:1.20 -y          # [CONFIRM]
fi

# Install  # [CONFIRM]
dnf install -y nginx

# Enable and start  # [CONFIRM]
systemctl enable --now nginx

# Open firewall  # [CONFIRM]
firewall-cmd --add-service=http --permanent
firewall-cmd --add-service=https --permanent
firewall-cmd --reload

Configuration Structure

/etc/nginx/
├── nginx.conf                    # Main config
├── conf.d/                       # Server blocks (*.conf auto-loaded)
│   └── default.conf
├── mime.types
└── modules-enabled/              # Dynamic modules (Rocky 9)

CORRECT -- use drop-in configs in conf.d/:

# WRONG: Putting everything in nginx.conf
# CORRECT: One file per site/app in /etc/nginx/conf.d/

Basic Server Block

# /etc/nginx/conf.d/example.conf  # [CONFIRM]
server {
    listen 80;
    listen [::]:80;
    server_name example.com www.example.com;

    root /var/www/example.com/public;
    index index.html index.php;

    # Logging
    access_log /var/log/nginx/example.com.access.log;
    error_log  /var/log/nginx/example.com.error.log;

    # Static files
    location /static/ {
        expires 30d;
        add_header Cache-Control "public, immutable";
    }

    # PHP-FPM (socket)
    location ~ \.php$ {
        fastcgi_pass unix:/run/php-fpm/www.sock;
        fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
        include fastcgi_params;
    }

    # Deny access to hidden files
    location ~ /\. {
        deny all;
    }
}

Nginx Security Headers

# Add to server block or http block  # [CONFIRM]
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 Content-Security-Policy "default-src 'self'" always;
add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always;

Nginx Operations

# Test configuration  # [READ-ONLY]
nginx -t

# Reload (graceful)  # [CONFIRM]
nginx -t && systemctl reload nginx

# Status  # [READ-ONLY]
systemctl status nginx
curl -s -o /dev/null -w "%{http_code}" http://localhost

# View logs  # [READ-ONLY]
journalctl -u nginx --since "1 hour ago"
tail -f /var/log/nginx/access.log
tail -f /var/log/nginx/error.log

ALWAYS test config before reload/restart:

# WRONG: Restart without testing
systemctl restart nginx

# CORRECT: Test first
nginx -t && systemctl reload nginx

Apache (httpd)

Installation

ROCKY_VERSION=$(source /etc/os-release && echo "${VERSION_ID%%.*}")

# Rocky 8: module stream  # [CONFIRM]
if [[ "$ROCKY_VERSION" == "8" ]]; then
    dnf module list httpd                    # [READ-ONLY]
fi

# Install  # [CONFIRM]
dnf install -y httpd mod_ssl

# Enable and start  # [CONFIRM]
systemctl enable --now httpd

# Open firewall  # [CONFIRM]
firewall-cmd --add-service=http --permanent
firewall-cmd --add-service=https --permanent
firewall-cmd --reload

Configuration Structure

/etc/httpd/
├── conf/
│   └── httpd.conf                # Main config
├── conf.d/                       # Drop-in configs (*.conf auto-loaded)
│   ├── ssl.conf
│   └── welcome.conf
├── conf.modules.d/               # Module loading
└── logs -> /var/log/httpd

Virtual Host

# /etc/httpd/conf.d/example.conf  # [CONFIRM]
<VirtualHost *:80>
    ServerName example.com
    ServerAlias www.example.com
    DocumentRoot /var/www/example.com/public

    <Directory /var/www/example.com/public>
        AllowOverride All
        Require all granted
    </Directory>

    # PHP-FPM via proxy
    <FilesMatch \.php$>
        SetHandler "proxy:unix:/run/php-fpm/www.sock|fcgi://localhost"
    </FilesMatch>

    ErrorLog /var/log/httpd/example.com-error.log
    CustomLog /var/log/httpd/example.com-access.log combined
</VirtualHost>

Apache Operations

# Test configuration  # [READ-ONLY]
apachectl -t
httpd -t

# Reload (graceful)  # [CONFIRM]
apachectl -t && systemctl reload httpd

# List loaded modules  # [READ-ONLY]
httpd -M

# Status  # [READ-ONLY]
systemctl status httpd

PHP-FPM

Installation

ROCKY_VERSION=$(source /etc/os-release && echo "${VERSION_ID%%.*}")

if [[ "$ROCKY_VERSION" == "8" ]]; then
    # Rocky 8: Choose PHP version via module stream  # [CONFIRM]
    dnf module reset php -y
    dnf module enable php:8.0 -y
    dnf install -y php php-fpm php-mysqlnd php-opcache php-json php-mbstring php-xml php-gd php-curl
elif [[ "$ROCKY_VERSION" == "9" ]]; then
    # Rocky 9: PHP 8.1+ available  # [CONFIRM]
    dnf install -y php php-fpm php-mysqlnd php-opcache php-mbstring php-xml php-gd php-curl
fi

# Enable and start  # [CONFIRM]
systemctl enable --now php-fpm

Pool Configuration

# /etc/php-fpm.d/www.conf (default pool)
# Or create: /etc/php-fpm.d/myapp.conf  # [CONFIRM]

[myapp]
user = myapp
group = myapp
listen = /run/php-fpm/myapp.sock
listen.owner = nginx           ; or apache
listen.group = nginx
listen.mode = 0660

; Process management
pm = dynamic
pm.max_children = 50
pm.start_servers = 5
pm.min_spare_servers = 5
pm.max_spare_servers = 35
pm.max_requests = 500          ; Recycle workers to prevent memory leaks

; Logging
php_admin_flag[log_errors] = on
php_admin_value[error_log] = /var/log/php-fpm/myapp-error.log
slowlog = /var/log/php-fpm/myapp-slow.log
request_slowlog_timeout = 5s

; Security
php_admin_value[open_basedir] = /opt/myapp:/tmp
php_admin_value[disable_functions] = exec,passthru,shell_exec,system,proc_open,popen

Socket vs TCP

Method Config When to Use
Unix socket listen = /run/php-fpm/www.sock Web server on same host (faster)
TCP listen = 127.0.0.1:9000 Web server on different host

OPcache Configuration

# /etc/php.d/10-opcache.ini  # [CONFIRM]
opcache.enable=1
opcache.memory_consumption=256
opcache.interned_strings_buffer=16
opcache.max_accelerated_files=10000
opcache.revalidate_freq=2
opcache.validate_timestamps=1         ; Set to 0 in production for max performance
opcache.save_comments=1
opcache.fast_shutdown=1

PHP Operations

# Check PHP version and modules  # [READ-ONLY]
php -v
php -m
php -i | grep opcache

# Check PHP-FPM config  # [READ-ONLY]
php-fpm -t

# Reload PHP-FPM  # [CONFIRM]
php-fpm -t && systemctl reload php-fpm

# PHP-FPM status (if enabled in pool config)  # [READ-ONLY]
curl http://localhost/status      # Requires pool status config

MariaDB

Installation

ROCKY_VERSION=$(source /etc/os-release && echo "${VERSION_ID%%.*}")

if [[ "$ROCKY_VERSION" == "8" ]]; then
    # Rocky 8: Choose module stream  # [CONFIRM]
    dnf module enable mariadb:10.5 -y
fi

# Install  # [CONFIRM]
dnf install -y mariadb-server

# Enable and start  # [CONFIRM]
systemctl enable --now mariadb

# Secure installation  # [CONFIRM]
mariadb-secure-installation
# Set root password, remove anonymous users, disable remote root, remove test DB

Configuration

# /etc/my.cnf.d/server.cnf  # [CONFIRM]
[mysqld]
# Network
bind-address = 127.0.0.1         # Local only (use 0.0.0.0 for remote)
port = 3306

# Character set
character-set-server = utf8mb4
collation-server = utf8mb4_unicode_ci

# InnoDB
innodb_buffer_pool_size = 1G     # 50-70% of available RAM for dedicated DB
innodb_log_file_size = 256M
innodb_flush_log_at_trx_commit = 1
innodb_file_per_table = ON

# Logging
log_error = /var/log/mariadb/mariadb.log
slow_query_log = ON
slow_query_log_file = /var/log/mariadb/slow.log
long_query_time = 2

# Safety
max_connections = 200
max_allowed_packet = 64M

User Management

-- Create database  -- [CONFIRM]
CREATE DATABASE myapp CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;

-- Create user with specific host  -- [CONFIRM]
CREATE USER 'myapp'@'localhost' IDENTIFIED BY 'secure_password_here';
GRANT ALL PRIVILEGES ON myapp.* TO 'myapp'@'localhost';
FLUSH PRIVILEGES;

-- Create read-only user  -- [CONFIRM]
CREATE USER 'reader'@'10.0.0.%' IDENTIFIED BY 'secure_password_here';
GRANT SELECT ON myapp.* TO 'reader'@'10.0.0.%';
FLUSH PRIVILEGES;

-- View grants  -- [READ-ONLY]
SHOW GRANTS FOR 'myapp'@'localhost';

WRONG -- granting ALL on *.*:

-- WRONG: Grants global admin privileges
GRANT ALL PRIVILEGES ON *.* TO 'myapp'@'localhost';

-- CORRECT: Grant only on the app database
GRANT ALL PRIVILEGES ON myapp.* TO 'myapp'@'localhost';

Backup and Restore

# Backup single database (consistent snapshot)  # [CONFIRM]
mariadb-dump --single-transaction --routines --triggers \
  myapp > /backup/myapp_$(date +%Y%m%d%H%M%S).sql

# Backup all databases  # [CONFIRM]
mariadb-dump --single-transaction --routines --triggers \
  --all-databases > /backup/all_$(date +%Y%m%d%H%M%S).sql

# Restore  # [DESTRUCTIVE]
mariadb myapp < /backup/myapp_20240115.sql

# Check database  # [READ-ONLY]
mysqlcheck --all-databases
mysqlcheck myapp

MariaDB Operations

# Status  # [READ-ONLY]
systemctl status mariadb
mysqladmin status
mysqladmin processlist

# Variables  # [READ-ONLY]
mariadb -e "SHOW VARIABLES LIKE 'innodb_buffer_pool_size';"
mariadb -e "SHOW GLOBAL STATUS LIKE 'Threads_%';"

SSL/TLS with Certbot

Installation

# Install certbot  # [CONFIRM]
dnf install -y epel-release
dnf install -y certbot

# For Nginx  # [CONFIRM]
dnf install -y python3-certbot-nginx

# For Apache  # [CONFIRM]
dnf install -y python3-certbot-apache

Obtain Certificate

# Nginx (auto-configures)  # [CONFIRM]
certbot --nginx -d example.com -d www.example.com

# Apache (auto-configures)  # [CONFIRM]
certbot --apache -d example.com -d www.example.com

# Standalone (when no web server is running)  # [CONFIRM]
certbot certonly --standalone -d example.com

# Webroot (web server running, no auto-config)  # [CONFIRM]
certbot certonly --webroot -w /var/www/example.com/public -d example.com

Auto-Renewal

# Test renewal  # [READ-ONLY]
certbot renew --dry-run

# Certbot creates a systemd timer automatically
systemctl status certbot-renew.timer    # [READ-ONLY]
systemctl list-timers certbot*          # [READ-ONLY]

# Or create timer if not present  # [CONFIRM]
# Timer is usually at /etc/systemd/system/certbot-renew.timer

Manual TLS Configuration (Nginx)

# /etc/nginx/conf.d/example-ssl.conf  # [CONFIRM]
server {
    listen 443 ssl http2;
    listen [::]:443 ssl http2;
    server_name example.com www.example.com;

    ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;

    # TLS hardening
    ssl_protocols TLSv1.2 TLSv1.3;
    ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384;
    ssl_prefer_server_ciphers off;
    ssl_session_timeout 1d;
    ssl_session_cache shared:SSL:50m;
    ssl_session_tickets off;

    # OCSP stapling
    ssl_stapling on;
    ssl_stapling_verify on;
    resolver 1.1.1.1 8.8.8.8 valid=300s;

    root /var/www/example.com/public;
    index index.html index.php;
}

# HTTP to HTTPS redirect
server {
    listen 80;
    listen [::]:80;
    server_name example.com www.example.com;
    return 301 https://$host$request_uri;
}

Reverse Proxy Patterns

Basic Reverse Proxy (Nginx)

# /etc/nginx/conf.d/app-proxy.conf  # [CONFIRM]
upstream app_backend {
    server 127.0.0.1:3000;
    # For multiple backends:
    # server 127.0.0.1:3001;
    # server 127.0.0.1:3002;
}

server {
    listen 443 ssl http2;
    server_name app.example.com;

    ssl_certificate /etc/letsencrypt/live/app.example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/app.example.com/privkey.pem;

    location / {
        proxy_pass http://app_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;

        # Timeouts
        proxy_connect_timeout 60s;
        proxy_send_timeout 60s;
        proxy_read_timeout 60s;
    }
}

WebSocket Proxy

# WebSocket support  # [CONFIRM]
map $http_upgrade $connection_upgrade {
    default upgrade;
    ''      close;
}

server {
    listen 443 ssl http2;
    server_name ws.example.com;

    # ... SSL config ...

    location /ws {
        proxy_pass http://127.0.0.1:3000;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection $connection_upgrade;
        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 86400;
    }
}

SELinux for Reverse Proxy

# Required for Nginx/Apache to proxy to backend services  # [CONFIRM]
setsebool -P httpd_can_network_connect on

Checklist: New Web Application Deployment

  • [ ] Install web server (Nginx or Apache)
  • [ ] Install PHP-FPM with required extensions
  • [ ] Configure PHP-FPM pool (socket, user, resource limits)
  • [ ] Configure OPcache
  • [ ] Install and configure MariaDB
  • [ ] Run mariadb-secure-installation
  • [ ] Create app database and user (least privilege)
  • [ ] Configure web server virtual host
  • [ ] Set up TLS with certbot
  • [ ] Configure HTTP to HTTPS redirect
  • [ ] Set security headers
  • [ ] Set SELinux contexts for document root
  • [ ] Enable httpd_can_network_connect if proxying
  • [ ] Open firewall ports (80, 443)
  • [ ] Test config (nginx -t / apachectl -t)
  • [ ] Verify auto-renewal (certbot renew --dry-run)

When to Use This Skill

  • Setting up web servers (Nginx or Apache)
  • Configuring PHP-FPM pools
  • Administering MariaDB (users, backups, tuning)
  • Obtaining and managing TLS certificates
  • Setting up reverse proxies (HTTP, WebSocket)
  • Troubleshooting web application issues
  • rocky-foundation -- OS detection, safety tiers
  • rocky-selinux -- Web server SELinux contexts and booleans
  • rocky-security-hardening -- TLS hardening, security headers
  • rocky-core-system -- Module streams for PHP, MariaDB, Nginx versions
  • rocky-networking -- Firewall rules for web ports

# Supported AI Coding Agents

This skill is compatible with the SKILL.md standard and works with all major AI coding agents:

Learn more about the SKILL.md standard and how to use these skills with your preferred AI coding agent.