Manage Apple Reminders via the `remindctl` CLI on macOS (list, add, edit, complete, delete)....
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_connectif 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
Related Skills
- 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.