Add webserver provisioning + vhost scripts, README, cheatsheet
- setup-webserver.sh: idempotent Ubuntu 24.04 LAMP provisioning (Apache event MPM + PHP 8.3-FPM + MariaDB + Node/Python, phpMyAdmin, Composer, Certbot, UFW, Fail2ban; optional components prompted/env-gated) - add-vhost.sh: add an Apache virtual host, optional DB + TLS - CHEATSHEET.md: day-to-day server CLI reference - README.md: setup instructions and env-var matrix Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
+162
@@ -0,0 +1,162 @@
|
|||||||
|
# Web Server CLI Cheatsheet — Ubuntu 24.04 (Apache + PHP-FPM + MariaDB)
|
||||||
|
|
||||||
|
Daily commands for running the server built by `setup-webserver.sh`.
|
||||||
|
|
||||||
|
## Quick status
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo healthcheck # custom: all services, disk, memory, ports
|
||||||
|
systemctl status apache2 # one service detail
|
||||||
|
sudo systemctl --failed # anything broken?
|
||||||
|
htop # live CPU/RAM/process (q to quit)
|
||||||
|
df -h # disk space
|
||||||
|
free -h # memory + swap
|
||||||
|
```
|
||||||
|
|
||||||
|
## Apache
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo systemctl reload apache2 # apply config, no dropped connections (PREFER)
|
||||||
|
sudo systemctl restart apache2 # full restart (drops connections)
|
||||||
|
sudo apache2ctl configtest # check config BEFORE reload — always do this
|
||||||
|
apache2ctl -v # version
|
||||||
|
|
||||||
|
# Manage sites (vhosts)
|
||||||
|
sudo a2ensite SITE.conf # enable a site
|
||||||
|
sudo a2dissite SITE.conf # disable a site
|
||||||
|
sudo a2enmod rewrite # enable a module
|
||||||
|
ls /etc/apache2/sites-available/ # all defined sites
|
||||||
|
ls /etc/apache2/sites-enabled/ # active sites (symlinks)
|
||||||
|
|
||||||
|
# Add a site (your script)
|
||||||
|
sudo ./add-vhost.sh example.com
|
||||||
|
|
||||||
|
# Logs (live tail, Ctrl+C to stop)
|
||||||
|
sudo tail -f /var/log/apache2/error.log
|
||||||
|
sudo tail -f /var/log/apache2/example.com-access.log
|
||||||
|
```
|
||||||
|
|
||||||
|
## PHP / PHP-FPM
|
||||||
|
|
||||||
|
```bash
|
||||||
|
php -v # version
|
||||||
|
php -m # installed modules
|
||||||
|
sudo systemctl restart php8.3-fpm # restart after ini changes
|
||||||
|
php -i | grep opcache # check opcache settings
|
||||||
|
sudo tail -f /var/log/php8.3-fpm.log # FPM errors
|
||||||
|
|
||||||
|
# Config locations
|
||||||
|
/etc/php/8.3/fpm/php.ini # main FPM config
|
||||||
|
/etc/php/8.3/fpm/conf.d/ # drop-in .ini files
|
||||||
|
/etc/php/8.3/fpm/pool.d/www.conf # worker pool tuning
|
||||||
|
```
|
||||||
|
|
||||||
|
## MariaDB / MySQL
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo mariadb # connect as root (socket auth, no password)
|
||||||
|
|
||||||
|
# Inside the mariadb prompt (end each with ;)
|
||||||
|
SHOW DATABASES;
|
||||||
|
USE mydb;
|
||||||
|
SHOW TABLES;
|
||||||
|
SELECT user, host FROM mysql.user;
|
||||||
|
\q # quit
|
||||||
|
|
||||||
|
# One-liners from shell
|
||||||
|
sudo mariadb -e "SHOW DATABASES;"
|
||||||
|
sudo mariadb mydb < dump.sql # import
|
||||||
|
sudo mariadb-dump mydb > dump.sql # export single DB
|
||||||
|
sudo db-backup # your nightly backup, run manually
|
||||||
|
ls -lh /var/backups/mysql/ # backups
|
||||||
|
|
||||||
|
# Create app DB + user
|
||||||
|
sudo mariadb -e "CREATE DATABASE app CHARACTER SET utf8mb4;
|
||||||
|
CREATE USER 'app'@'localhost' IDENTIFIED BY 'CHANGE_ME';
|
||||||
|
GRANT ALL ON app.* TO 'app'@'localhost'; FLUSH PRIVILEGES;"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Firewall (UFW)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo ufw status verbose # rules + active?
|
||||||
|
sudo ufw allow 8080/tcp # open a port
|
||||||
|
sudo ufw delete allow 8080/tcp # close it
|
||||||
|
sudo ufw deny from 1.2.3.4 # block an IP
|
||||||
|
```
|
||||||
|
|
||||||
|
## Fail2ban (brute-force bans)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo fail2ban-client status # active jails
|
||||||
|
sudo fail2ban-client status sshd # banned IPs for SSH
|
||||||
|
sudo fail2ban-client set sshd unbanip 1.2.3.4 # unban
|
||||||
|
```
|
||||||
|
|
||||||
|
## TLS / Certbot
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo certbot --apache -d example.com -d www.example.com # get cert
|
||||||
|
sudo certbot certificates # list certs + expiry
|
||||||
|
sudo certbot renew --dry-run # test auto-renew (real renew is automatic)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Services (systemd) — the universal pattern
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo systemctl start|stop|restart|reload|status NAME
|
||||||
|
sudo systemctl enable NAME # start on boot
|
||||||
|
sudo systemctl disable NAME # don't start on boot
|
||||||
|
journalctl -u NAME -f # live logs for any service
|
||||||
|
journalctl -u NAME --since "1 hour ago"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Files & permissions (web root)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo chown -R www-data:www-data /var/www/example.com # web server owns files
|
||||||
|
sudo find /var/www -type d -exec chmod 755 {} \; # dirs
|
||||||
|
sudo find /var/www -type f -exec chmod 644 {} \; # files
|
||||||
|
du -sh /var/www/* # folder sizes
|
||||||
|
```
|
||||||
|
|
||||||
|
## Node (if installed)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
node -v ; npm -v
|
||||||
|
pm2 start app.js --name myapp # run a node app, kept alive
|
||||||
|
pm2 list # running apps
|
||||||
|
pm2 logs myapp
|
||||||
|
pm2 restart myapp
|
||||||
|
pm2 startup && pm2 save # survive reboot
|
||||||
|
```
|
||||||
|
|
||||||
|
## System maintenance
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo apt update && sudo apt upgrade -y # update packages
|
||||||
|
sudo apt autoremove # clean unused
|
||||||
|
sudo reboot
|
||||||
|
uptime # how long up + load
|
||||||
|
who # who's logged in
|
||||||
|
last # login history
|
||||||
|
```
|
||||||
|
|
||||||
|
## Logs — where to look when something breaks
|
||||||
|
|
||||||
|
| Problem | Look here |
|
||||||
|
|---------|-----------|
|
||||||
|
| Site 500 error | `/var/log/apache2/<site>-error.log` |
|
||||||
|
| PHP crash | `/var/log/php8.3-fpm.log` |
|
||||||
|
| DB won't start | `journalctl -u mariadb` |
|
||||||
|
| Can't SSH in | `journalctl -u ssh` (from console) |
|
||||||
|
| Service down | `systemctl status <name>` |
|
||||||
|
| Anything else | `journalctl -xe` |
|
||||||
|
|
||||||
|
## Survival tips
|
||||||
|
|
||||||
|
- **Always `configtest` before reloading Apache.** Bad config + restart = site down.
|
||||||
|
- **`reload` over `restart`** when possible — no dropped connections.
|
||||||
|
- **Keep an SSH session open** when changing SSH/firewall config. Test new login in a *second* terminal before closing the first.
|
||||||
|
- **`Ctrl+C`** stops a running command (like `tail -f`). **`q`** quits pagers (`less`, `htop`).
|
||||||
|
- **Tab** autocompletes paths/commands. **↑** recalls last command.
|
||||||
@@ -0,0 +1,114 @@
|
|||||||
|
# webserver
|
||||||
|
|
||||||
|
Provisioning + virtual-host scripts for a LAMP-style web server on **Ubuntu 24.04 LTS**.
|
||||||
|
|
||||||
|
Stack: **Apache** (event MPM) + **PHP 8.3-FPM** + **MariaDB** + Node + Python, with
|
||||||
|
phpMyAdmin (IP-restricted), Composer, Certbot, UFW and Fail2ban. `.htaccess` works out
|
||||||
|
of the box (`mod_rewrite` + `AllowOverride All`).
|
||||||
|
|
||||||
|
## Contents
|
||||||
|
|
||||||
|
| File | What it does |
|
||||||
|
|------|--------------|
|
||||||
|
| [`setup-webserver.sh`](setup-webserver.sh) | One-shot provisioning of a fresh server. Idempotent — safe to re-run. |
|
||||||
|
| [`add-vhost.sh`](add-vhost.sh) | Add an Apache virtual host (+ optional DB + TLS) for a domain. |
|
||||||
|
| [`CHEATSHEET.md`](CHEATSHEET.md) | Day-to-day CLI commands for running the server. |
|
||||||
|
|
||||||
|
## Quick start
|
||||||
|
|
||||||
|
On a fresh Ubuntu 24.04 box, as root (or with `sudo`):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 1. Get the scripts
|
||||||
|
git clone https://git.rkeus.com/rkeus/webserver.git
|
||||||
|
cd webserver
|
||||||
|
|
||||||
|
# 2. Provision the server (prompts for each optional component)
|
||||||
|
sudo bash setup-webserver.sh
|
||||||
|
|
||||||
|
# 3. Verify, then DELETE the test page
|
||||||
|
# http://<server-ip>/info.php
|
||||||
|
sudo rm /var/www/html/info.php
|
||||||
|
```
|
||||||
|
|
||||||
|
### Add a site
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Interactive
|
||||||
|
sudo bash add-vhost.sh
|
||||||
|
|
||||||
|
# Domain as arg, rest prompted
|
||||||
|
sudo bash add-vhost.sh example.com
|
||||||
|
|
||||||
|
# Domain + web root
|
||||||
|
sudo bash add-vhost.sh example.com /var/www/example.com
|
||||||
|
```
|
||||||
|
|
||||||
|
After DNS points at the server, get HTTPS:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo certbot --apache -d example.com -d www.example.com
|
||||||
|
```
|
||||||
|
|
||||||
|
## Non-interactive / automation
|
||||||
|
|
||||||
|
Both scripts read environment variables so prompts are skipped — good for CI or
|
||||||
|
unattended runs.
|
||||||
|
|
||||||
|
`setup-webserver.sh` toggles (`yes` | `no`):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo INSTALL_NODE=no INSTALL_SWAP=yes HARDEN_SSH=no bash setup-webserver.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
| Var | Default | Component |
|
||||||
|
|-----|---------|-----------|
|
||||||
|
| `INSTALL_PHPMYADMIN` | yes | phpMyAdmin, IP-restricted |
|
||||||
|
| `INSTALL_NODE` | yes | Node.js + PM2 |
|
||||||
|
| `INSTALL_REDIS` | yes | Redis + php-redis |
|
||||||
|
| `INSTALL_CERTBOT` | yes | Certbot (cert requested later) |
|
||||||
|
| `INSTALL_SWAP` | yes if RAM<4GB | swapfile |
|
||||||
|
| `INSTALL_DB_BACKUP` | yes | nightly mysqldump cron |
|
||||||
|
| `TUNE_PHP` | yes | OPcache + FPM pool tuning |
|
||||||
|
| `INSTALL_HEALTHCHECK` | yes | `healthcheck` command |
|
||||||
|
| `HARDEN_SSH` | no | key-only SSH (**lockout risk**) |
|
||||||
|
|
||||||
|
Other config (env, with defaults):
|
||||||
|
|
||||||
|
| Var | Default | Meaning |
|
||||||
|
|-----|---------|---------|
|
||||||
|
| `SERVER_NAME` | `_` | default vhost ServerName (catch-all) |
|
||||||
|
| `ADMIN_EMAIL` | `admin@example.com` | certbot contact |
|
||||||
|
| `TIMEZONE` | `UTC` | system timezone |
|
||||||
|
| `PMA_ALLOW_IPS` | `127.0.0.1` | IPs/CIDRs allowed to reach phpMyAdmin |
|
||||||
|
| `NODE_MAJOR` | `22` | NodeSource LTS line |
|
||||||
|
|
||||||
|
`add-vhost.sh` non-interactive:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo DOMAIN=example.com MAKE_DB=yes RUN_TLS=yes bash add-vhost.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
| Var | Default | Meaning |
|
||||||
|
|-----|---------|---------|
|
||||||
|
| `DOMAIN` | — (required) | the site domain |
|
||||||
|
| `WEB_ROOT` | `/var/www/<domain>` | document root |
|
||||||
|
| `ADMIN_EMAIL` | `admin@<domain>` | TLS contact |
|
||||||
|
| `MAKE_DB` | no | create matching MariaDB DB + user |
|
||||||
|
| `RUN_TLS` | no | request Let's Encrypt cert now (DNS must resolve) |
|
||||||
|
|
||||||
|
When `MAKE_DB=yes`, generated DB credentials are written to
|
||||||
|
`/root/<domain>.db-credentials.txt` (root-only, `chmod 600`).
|
||||||
|
|
||||||
|
## Notes & safety
|
||||||
|
|
||||||
|
- **Run as root.** Both scripts refuse to run otherwise.
|
||||||
|
- **Tested on Ubuntu 24.04.** Other versions warn and continue.
|
||||||
|
- **MariaDB root** uses `unix_socket` auth — connect locally with `sudo mariadb`, no password.
|
||||||
|
- **phpMyAdmin is not public.** Defaults to localhost only; set `PMA_ALLOW_IPS` or use an
|
||||||
|
SSH tunnel: `ssh -L 8080:localhost:80 user@server` then `http://localhost:8080/phpmyadmin`.
|
||||||
|
- **`HARDEN_SSH=yes` can lock you out.** It refuses unless an `authorized_keys` already
|
||||||
|
exists. Keep an SSH session open and test a new login before closing it.
|
||||||
|
- After provisioning, **delete** `/var/www/html/info.php`.
|
||||||
|
|
||||||
|
See [`CHEATSHEET.md`](CHEATSHEET.md) for the day-to-day command reference.
|
||||||
+160
@@ -0,0 +1,160 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
#
|
||||||
|
# add-vhost.sh — Add an Apache virtual host on Ubuntu 24.04
|
||||||
|
#
|
||||||
|
# Hybrid: pass args to skip prompts, omit them to be asked.
|
||||||
|
#
|
||||||
|
# sudo ./add-vhost.sh # fully interactive
|
||||||
|
# sudo ./add-vhost.sh example.com # domain set, rest prompted
|
||||||
|
# sudo ./add-vhost.sh example.com /var/www/ex # domain + root set
|
||||||
|
#
|
||||||
|
# Non-interactive (CI / scripted) — set env vars, prompts auto-skip:
|
||||||
|
# sudo DOMAIN=example.com MAKE_DB=yes RUN_TLS=yes ./add-vhost.sh
|
||||||
|
#
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
PHP_VER="8.3"
|
||||||
|
|
||||||
|
log() { printf '\n\033[1;32m==> %s\033[0m\n' "$*"; }
|
||||||
|
warn() { printf '\033[1;33m!! %s\033[0m\n' "$*"; }
|
||||||
|
die() { printf '\033[1;31mXX %s\033[0m\n' "$*" >&2; exit 1; }
|
||||||
|
|
||||||
|
[[ $EUID -eq 0 ]] || die "Run as root: sudo bash $0"
|
||||||
|
|
||||||
|
# Prompt only if stdin is a terminal; else rely on env/args (non-interactive).
|
||||||
|
interactive() { [[ -t 0 ]]; }
|
||||||
|
|
||||||
|
# ask VAR "Question" "default" — sets VAR from env, else arg-passed, else prompt, else default
|
||||||
|
ask() {
|
||||||
|
local __var="$1" __q="$2" __def="${3:-}" __cur="${!1:-}"
|
||||||
|
[[ -n "$__cur" ]] && return # already set (env or earlier arg)
|
||||||
|
if interactive; then
|
||||||
|
local __ans
|
||||||
|
read -rp "$__q${__def:+ [$__def]}: " __ans
|
||||||
|
printf -v "$__var" '%s' "${__ans:-$__def}"
|
||||||
|
else
|
||||||
|
printf -v "$__var" '%s' "$__def"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
ask_yn() { # ask_yn VAR "Question" default(yes/no)
|
||||||
|
local __var="$1" __q="$2" __def="${3:-no}" __cur="${!1:-}"
|
||||||
|
[[ -n "$__cur" ]] && return
|
||||||
|
if interactive; then
|
||||||
|
local __ans
|
||||||
|
read -rp "$__q [$( [[ $__def == yes ]] && echo Y/n || echo y/N )]: " __ans
|
||||||
|
__ans="${__ans:-$__def}"
|
||||||
|
case "$__ans" in [Yy]*) printf -v "$__var" yes;; *) printf -v "$__var" no;; esac
|
||||||
|
else
|
||||||
|
printf -v "$__var" '%s' "$__def"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
#─────────────────────────────────────────────────────────────────────────────
|
||||||
|
# Gather inputs: positional args win, then env, then prompt
|
||||||
|
#─────────────────────────────────────────────────────────────────────────────
|
||||||
|
DOMAIN="${DOMAIN:-${1:-}}"
|
||||||
|
WEB_ROOT="${WEB_ROOT:-${2:-}}"
|
||||||
|
|
||||||
|
ask DOMAIN "Domain (e.g. example.com)"
|
||||||
|
[[ -n "$DOMAIN" ]] || die "Domain required"
|
||||||
|
# Basic sanity: letters/digits/dots/hyphens only
|
||||||
|
[[ "$DOMAIN" =~ ^[A-Za-z0-9.-]+$ ]] || die "Invalid domain: $DOMAIN"
|
||||||
|
|
||||||
|
ask WEB_ROOT "Web root" "/var/www/${DOMAIN}"
|
||||||
|
ask ADMIN_EMAIL "Admin email (for TLS)" "admin@${DOMAIN}"
|
||||||
|
ask_yn MAKE_DB "Create matching MySQL database + user?" no
|
||||||
|
ask_yn RUN_TLS "Request Let's Encrypt TLS now (DNS must point here)?" no
|
||||||
|
|
||||||
|
VHOST_FILE="/etc/apache2/sites-available/${DOMAIN}.conf"
|
||||||
|
|
||||||
|
#─────────────────────────────────────────────────────────────────────────────
|
||||||
|
log "Web root: ${WEB_ROOT}"
|
||||||
|
#─────────────────────────────────────────────────────────────────────────────
|
||||||
|
mkdir -p "$WEB_ROOT"
|
||||||
|
if [[ ! -f "${WEB_ROOT}/index.html" && -z "$(ls -A "$WEB_ROOT" 2>/dev/null)" ]]; then
|
||||||
|
echo "<h1>${DOMAIN} works</h1>" > "${WEB_ROOT}/index.html"
|
||||||
|
fi
|
||||||
|
chown -R www-data:www-data "$WEB_ROOT"
|
||||||
|
|
||||||
|
#─────────────────────────────────────────────────────────────────────────────
|
||||||
|
log "Write vhost: ${VHOST_FILE}"
|
||||||
|
#─────────────────────────────────────────────────────────────────────────────
|
||||||
|
[[ -f "$VHOST_FILE" ]] && warn "Overwriting existing ${VHOST_FILE}"
|
||||||
|
cat > "$VHOST_FILE" <<EOF
|
||||||
|
<VirtualHost *:80>
|
||||||
|
ServerName ${DOMAIN}
|
||||||
|
ServerAlias www.${DOMAIN}
|
||||||
|
DocumentRoot ${WEB_ROOT}
|
||||||
|
|
||||||
|
<Directory ${WEB_ROOT}>
|
||||||
|
Options -Indexes +FollowSymLinks
|
||||||
|
AllowOverride All
|
||||||
|
Require all granted
|
||||||
|
</Directory>
|
||||||
|
|
||||||
|
ErrorLog \${APACHE_LOG_DIR}/${DOMAIN}-error.log
|
||||||
|
CustomLog \${APACHE_LOG_DIR}/${DOMAIN}-access.log combined
|
||||||
|
</VirtualHost>
|
||||||
|
EOF
|
||||||
|
|
||||||
|
a2ensite -q "${DOMAIN}.conf"
|
||||||
|
apache2ctl configtest || die "Apache config test failed — fix before reload"
|
||||||
|
systemctl reload apache2
|
||||||
|
log "Site enabled + Apache reloaded"
|
||||||
|
|
||||||
|
#─────────────────────────────────────────────────────────────────────────────
|
||||||
|
if [[ "$MAKE_DB" == "yes" ]]; then
|
||||||
|
log "Create database + user"
|
||||||
|
# DB name from domain: example.com -> example_com
|
||||||
|
DB_NAME="$(echo "$DOMAIN" | tr '.-' '__')"
|
||||||
|
DB_USER="${DB_NAME:0:32}" # MariaDB user max 32 chars (10.4+ allows 80, stay safe)
|
||||||
|
# Generate random password
|
||||||
|
DB_PASS="$(openssl rand -base64 18 | tr -d '/+=' | head -c 20)"
|
||||||
|
|
||||||
|
mariadb <<SQL
|
||||||
|
CREATE DATABASE IF NOT EXISTS \`${DB_NAME}\` CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
|
||||||
|
CREATE USER IF NOT EXISTS '${DB_USER}'@'localhost' IDENTIFIED BY '${DB_PASS}';
|
||||||
|
GRANT ALL PRIVILEGES ON \`${DB_NAME}\`.* TO '${DB_USER}'@'localhost';
|
||||||
|
FLUSH PRIVILEGES;
|
||||||
|
SQL
|
||||||
|
# Save creds to a root-only file next to web root
|
||||||
|
CRED_FILE="/root/${DOMAIN}.db-credentials.txt"
|
||||||
|
cat > "$CRED_FILE" <<EOF
|
||||||
|
Domain : ${DOMAIN}
|
||||||
|
Database : ${DB_NAME}
|
||||||
|
Username : ${DB_USER}
|
||||||
|
Password : ${DB_PASS}
|
||||||
|
Host : localhost
|
||||||
|
EOF
|
||||||
|
chmod 600 "$CRED_FILE"
|
||||||
|
warn "DB created. Credentials saved: ${CRED_FILE} (root-only)"
|
||||||
|
echo " DB=${DB_NAME} USER=${DB_USER} PASS=${DB_PASS}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
#─────────────────────────────────────────────────────────────────────────────
|
||||||
|
if [[ "$RUN_TLS" == "yes" ]]; then
|
||||||
|
log "Request TLS certificate (certbot)"
|
||||||
|
if command -v certbot >/dev/null; then
|
||||||
|
certbot --apache -d "$DOMAIN" -d "www.${DOMAIN}" \
|
||||||
|
-m "$ADMIN_EMAIL" --agree-tos --redirect -n \
|
||||||
|
|| warn "Certbot failed — check DNS points to this server, retry manually"
|
||||||
|
else
|
||||||
|
warn "certbot not installed. Install: apt install certbot python3-certbot-apache"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
#─────────────────────────────────────────────────────────────────────────────
|
||||||
|
log "Done: ${DOMAIN}"
|
||||||
|
#─────────────────────────────────────────────────────────────────────────────
|
||||||
|
cat <<EOF
|
||||||
|
|
||||||
|
Domain : http://${DOMAIN}$( [[ $RUN_TLS == yes ]] && echo " (https enabled)" )
|
||||||
|
Web root : ${WEB_ROOT}
|
||||||
|
Vhost : ${VHOST_FILE}
|
||||||
|
Logs : /var/log/apache2/${DOMAIN}-{error,access}.log
|
||||||
|
$( [[ $MAKE_DB == yes ]] && echo " DB creds : /root/${DOMAIN}.db-credentials.txt" )
|
||||||
|
|
||||||
|
No TLS yet? After DNS points here:
|
||||||
|
certbot --apache -d ${DOMAIN} -d www.${DOMAIN} -m ${ADMIN_EMAIL} --agree-tos --redirect
|
||||||
|
EOF
|
||||||
@@ -0,0 +1,430 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
#
|
||||||
|
# setup-webserver.sh — Fresh web server provisioning for Ubuntu 24.04 LTS
|
||||||
|
#
|
||||||
|
# Stack: Apache (event MPM) + PHP 8.3-FPM + MariaDB + Node + Python
|
||||||
|
# + phpMyAdmin (IP-restricted) + Composer + Certbot + UFW + Fail2ban
|
||||||
|
#
|
||||||
|
# .htaccess works: mod_rewrite enabled, AllowOverride All on web root.
|
||||||
|
#
|
||||||
|
# Idempotent: safe to re-run. Each section guarded.
|
||||||
|
# Run as root or with sudo: sudo bash setup-webserver.sh
|
||||||
|
#
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
#─────────────────────────────────────────────────────────────────────────────
|
||||||
|
# CONFIG — edit these before running
|
||||||
|
#─────────────────────────────────────────────────────────────────────────────
|
||||||
|
WEB_ROOT="/var/www/html"
|
||||||
|
SERVER_NAME="${SERVER_NAME:-_}" # e.g. example.com (default: catch-all)
|
||||||
|
ADMIN_EMAIL="${ADMIN_EMAIL:-admin@example.com}" # for certbot
|
||||||
|
TIMEZONE="${TIMEZONE:-UTC}"
|
||||||
|
|
||||||
|
# phpMyAdmin access restriction — only these IPs/CIDRs may reach it.
|
||||||
|
# Set to your office/home IP. Empty = localhost only (use SSH tunnel).
|
||||||
|
PMA_ALLOW_IPS="${PMA_ALLOW_IPS:-127.0.0.1}"
|
||||||
|
|
||||||
|
# Feature toggles — left UNSET so the script PROMPTS for each (Enter = default).
|
||||||
|
# Set any via env to skip its prompt (good for automation/CI):
|
||||||
|
# sudo INSTALL_NODE=no INSTALL_SWAP=yes bash setup-webserver.sh
|
||||||
|
# Values: yes | no
|
||||||
|
#
|
||||||
|
# INSTALL_PHPMYADMIN (default yes) phpMyAdmin, IP-restricted
|
||||||
|
# INSTALL_NODE (default yes) Node.js + PM2
|
||||||
|
# INSTALL_REDIS (default yes) Redis + php-redis
|
||||||
|
# INSTALL_CERTBOT (default yes) certbot (cert requested later, manually)
|
||||||
|
# INSTALL_SWAP (default yes if RAM<4GB) swapfile
|
||||||
|
# HARDEN_SSH (default no) key-only SSH <-- lockout risk, asks twice
|
||||||
|
# INSTALL_DB_BACKUP (default yes) nightly mysqldump cron
|
||||||
|
# TUNE_PHP (default yes) OPcache + FPM pool tuning
|
||||||
|
# INSTALL_HEALTHCHECK (default yes) /usr/local/bin/healthcheck
|
||||||
|
NODE_MAJOR="${NODE_MAJOR:-22}" # NodeSource LTS line
|
||||||
|
|
||||||
|
PHP_VER="8.3" # Ubuntu 24.04 default
|
||||||
|
|
||||||
|
#─────────────────────────────────────────────────────────────────────────────
|
||||||
|
# Helpers
|
||||||
|
#─────────────────────────────────────────────────────────────────────────────
|
||||||
|
log() { printf '\n\033[1;32m==> %s\033[0m\n' "$*"; }
|
||||||
|
warn() { printf '\033[1;33m!! %s\033[0m\n' "$*"; }
|
||||||
|
die() { printf '\033[1;31mXX %s\033[0m\n' "$*" >&2; exit 1; }
|
||||||
|
|
||||||
|
# Prompt only when stdin is a real terminal; else use env/default (CI-safe).
|
||||||
|
interactive() { [[ -t 0 ]]; }
|
||||||
|
|
||||||
|
# ask_yn VAR "Question" default(yes/no)
|
||||||
|
# - if VAR already set (via env): keep it, no prompt
|
||||||
|
# - else if interactive: ask, Enter = default
|
||||||
|
# - else (no TTY): use default
|
||||||
|
ask_yn() {
|
||||||
|
local __var="$1" __q="$2" __def="${3:-no}" __cur="${!1:-}"
|
||||||
|
[[ -n "$__cur" ]] && return
|
||||||
|
if interactive; then
|
||||||
|
local __ans
|
||||||
|
read -rp " ? $__q [$( [[ $__def == yes ]] && echo Y/n || echo y/N )]: " __ans
|
||||||
|
__ans="${__ans:-$__def}"
|
||||||
|
case "$__ans" in [Yy]*) printf -v "$__var" yes;; *) printf -v "$__var" no;; esac
|
||||||
|
else
|
||||||
|
printf -v "$__var" '%s' "$__def"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
[[ $EUID -eq 0 ]] || die "Run as root: sudo bash $0"
|
||||||
|
. /etc/os-release
|
||||||
|
[[ "${VERSION_ID:-}" == "24.04" ]] || warn "Tested on 24.04, found ${VERSION_ID:-unknown} — continuing"
|
||||||
|
|
||||||
|
export DEBIAN_FRONTEND=noninteractive
|
||||||
|
|
||||||
|
# Detect RAM (MB) to pick swap default
|
||||||
|
RAM_MB=$(free -m | awk '/^Mem:/{print $2}')
|
||||||
|
|
||||||
|
#─────────────────────────────────────────────────────────────────────────────
|
||||||
|
log "Choose what to install (Enter = default)"
|
||||||
|
#─────────────────────────────────────────────────────────────────────────────
|
||||||
|
ask_yn INSTALL_PHPMYADMIN "phpMyAdmin (IP-restricted web DB admin)?" yes
|
||||||
|
ask_yn INSTALL_NODE "Node.js ${NODE_MAJOR}.x + PM2?" yes
|
||||||
|
ask_yn INSTALL_REDIS "Redis cache + php-redis?" yes
|
||||||
|
ask_yn INSTALL_CERTBOT "Certbot (Let's Encrypt TLS tooling)?" yes
|
||||||
|
ask_yn INSTALL_SWAP "Swapfile (RAM safety, ${RAM_MB}MB detected)?" "$([[ $RAM_MB -lt 4000 ]] && echo yes || echo no)"
|
||||||
|
ask_yn INSTALL_DB_BACKUP "Nightly DB backup cron?" yes
|
||||||
|
ask_yn TUNE_PHP "Tune PHP OPcache + FPM pool?" yes
|
||||||
|
ask_yn INSTALL_HEALTHCHECK "Install healthcheck command?" yes
|
||||||
|
ask_yn HARDEN_SSH "Harden SSH (key-only, no root login)? RISK" no
|
||||||
|
|
||||||
|
#─────────────────────────────────────────────────────────────────────────────
|
||||||
|
log "System update + base tools"
|
||||||
|
#─────────────────────────────────────────────────────────────────────────────
|
||||||
|
apt-get update -y
|
||||||
|
apt-get upgrade -y
|
||||||
|
apt-get install -y \
|
||||||
|
curl wget git unzip zip ca-certificates gnupg lsb-release \
|
||||||
|
software-properties-common apt-transport-https \
|
||||||
|
htop vim ufw fail2ban unattended-upgrades \
|
||||||
|
build-essential pkg-config
|
||||||
|
|
||||||
|
timedatectl set-timezone "$TIMEZONE" || warn "timezone set failed"
|
||||||
|
|
||||||
|
#─────────────────────────────────────────────────────────────────────────────
|
||||||
|
log "Apache (event MPM) + mod_rewrite + proxy_fcgi"
|
||||||
|
#─────────────────────────────────────────────────────────────────────────────
|
||||||
|
apt-get install -y apache2
|
||||||
|
|
||||||
|
# Use event MPM (fast) with PHP-FPM, not prefork/mod_php.
|
||||||
|
a2dismod -q mpm_prefork 2>/dev/null || true
|
||||||
|
a2enmod -q mpm_event
|
||||||
|
a2enmod -q rewrite headers ssl proxy proxy_fcgi setenvif http2
|
||||||
|
|
||||||
|
#─────────────────────────────────────────────────────────────────────────────
|
||||||
|
log "PHP ${PHP_VER} + FPM + common extensions"
|
||||||
|
#─────────────────────────────────────────────────────────────────────────────
|
||||||
|
apt-get install -y \
|
||||||
|
php${PHP_VER} php${PHP_VER}-fpm php${PHP_VER}-cli \
|
||||||
|
php${PHP_VER}-mysql php${PHP_VER}-curl php${PHP_VER}-gd \
|
||||||
|
php${PHP_VER}-mbstring php${PHP_VER}-xml php${PHP_VER}-zip \
|
||||||
|
php${PHP_VER}-bcmath php${PHP_VER}-intl php${PHP_VER}-soap \
|
||||||
|
php${PHP_VER}-readline php${PHP_VER}-opcache
|
||||||
|
|
||||||
|
# Hand PHP to FPM
|
||||||
|
a2enconf -q php${PHP_VER}-fpm
|
||||||
|
systemctl enable --now php${PHP_VER}-fpm
|
||||||
|
|
||||||
|
#─────────────────────────────────────────────────────────────────────────────
|
||||||
|
log "Apache vhost — .htaccess enabled (AllowOverride All)"
|
||||||
|
#─────────────────────────────────────────────────────────────────────────────
|
||||||
|
mkdir -p "$WEB_ROOT"
|
||||||
|
cat > /etc/apache2/sites-available/000-default.conf <<EOF
|
||||||
|
<VirtualHost *:80>
|
||||||
|
ServerName ${SERVER_NAME}
|
||||||
|
DocumentRoot ${WEB_ROOT}
|
||||||
|
|
||||||
|
<Directory ${WEB_ROOT}>
|
||||||
|
Options -Indexes +FollowSymLinks
|
||||||
|
AllowOverride All
|
||||||
|
Require all granted
|
||||||
|
</Directory>
|
||||||
|
|
||||||
|
ErrorLog \${APACHE_LOG_DIR}/error.log
|
||||||
|
CustomLog \${APACHE_LOG_DIR}/access.log combined
|
||||||
|
</VirtualHost>
|
||||||
|
EOF
|
||||||
|
|
||||||
|
# phpinfo test page (delete after verifying)
|
||||||
|
[[ -f "${WEB_ROOT}/info.php" ]] || echo "<?php phpinfo();" > "${WEB_ROOT}/info.php"
|
||||||
|
chown -R www-data:www-data "$WEB_ROOT"
|
||||||
|
|
||||||
|
systemctl restart apache2
|
||||||
|
|
||||||
|
#─────────────────────────────────────────────────────────────────────────────
|
||||||
|
log "MariaDB (MySQL-compatible)"
|
||||||
|
#─────────────────────────────────────────────────────────────────────────────
|
||||||
|
apt-get install -y mariadb-server mariadb-client
|
||||||
|
systemctl enable --now mariadb
|
||||||
|
|
||||||
|
# Non-interactive hardening (mysql_secure_installation equivalent).
|
||||||
|
# Root uses unix_socket auth by default on 24.04 — no password needed locally.
|
||||||
|
mariadb <<'SQL'
|
||||||
|
DELETE FROM mysql.global_priv WHERE User='';
|
||||||
|
DELETE FROM mysql.global_priv WHERE User='root' AND Host NOT IN ('localhost','127.0.0.1','::1');
|
||||||
|
DROP DATABASE IF EXISTS test;
|
||||||
|
DELETE FROM mysql.db WHERE Db='test' OR Db='test\\_%';
|
||||||
|
FLUSH PRIVILEGES;
|
||||||
|
SQL
|
||||||
|
warn "MariaDB root uses unix_socket auth — connect with: sudo mariadb"
|
||||||
|
|
||||||
|
#─────────────────────────────────────────────────────────────────────────────
|
||||||
|
log "Composer (PHP dependency manager)"
|
||||||
|
#─────────────────────────────────────────────────────────────────────────────
|
||||||
|
if ! command -v composer >/dev/null; then
|
||||||
|
EXPECTED="$(curl -s https://composer.github.io/installer.sig)"
|
||||||
|
php -r "copy('https://getcomposer.org/installer','/tmp/composer-setup.php');"
|
||||||
|
ACTUAL="$(php -r "echo hash_file('sha384','/tmp/composer-setup.php');")"
|
||||||
|
[[ "$EXPECTED" == "$ACTUAL" ]] || die "Composer installer checksum mismatch"
|
||||||
|
php /tmp/composer-setup.php --install-dir=/usr/local/bin --filename=composer
|
||||||
|
rm -f /tmp/composer-setup.php
|
||||||
|
fi
|
||||||
|
|
||||||
|
#─────────────────────────────────────────────────────────────────────────────
|
||||||
|
log "Python 3 + pip + venv"
|
||||||
|
#─────────────────────────────────────────────────────────────────────────────
|
||||||
|
apt-get install -y python3 python3-pip python3-venv python3-dev
|
||||||
|
|
||||||
|
#─────────────────────────────────────────────────────────────────────────────
|
||||||
|
if [[ "$INSTALL_NODE" == "yes" ]]; then
|
||||||
|
log "Node.js ${NODE_MAJOR}.x (NodeSource) + PM2"
|
||||||
|
if ! command -v node >/dev/null || [[ "$(node -v 2>/dev/null)" != v${NODE_MAJOR}* ]]; then
|
||||||
|
curl -fsSL "https://deb.nodesource.com/setup_${NODE_MAJOR}.x" | bash -
|
||||||
|
apt-get install -y nodejs
|
||||||
|
fi
|
||||||
|
npm install -g pm2
|
||||||
|
fi
|
||||||
|
|
||||||
|
#─────────────────────────────────────────────────────────────────────────────
|
||||||
|
if [[ "$INSTALL_REDIS" == "yes" ]]; then
|
||||||
|
log "Redis (cache / sessions)"
|
||||||
|
apt-get install -y redis-server php${PHP_VER}-redis
|
||||||
|
systemctl enable --now redis-server
|
||||||
|
fi
|
||||||
|
|
||||||
|
#─────────────────────────────────────────────────────────────────────────────
|
||||||
|
if [[ "$INSTALL_PHPMYADMIN" == "yes" ]]; then
|
||||||
|
log "phpMyAdmin (IP-restricted — NOT public)"
|
||||||
|
# Preseed so apt install is non-interactive; reconfigure web server manually.
|
||||||
|
debconf-set-selections <<'PMA'
|
||||||
|
phpmyadmin phpmyadmin/dbconfig-install boolean true
|
||||||
|
phpmyadmin phpmyadmin/reconfigure-webserver multiselect apache2
|
||||||
|
phpmyadmin phpmyadmin/mysql/admin-pass password
|
||||||
|
phpmyadmin phpmyadmin/app-password-confirm password
|
||||||
|
phpmyadmin phpmyadmin/mysql/app-pass password
|
||||||
|
PMA
|
||||||
|
apt-get install -y phpmyadmin
|
||||||
|
|
||||||
|
# Lock down access by IP. Default localhost-only -> use SSH tunnel.
|
||||||
|
ALLOW_BLOCK=" Require ip ${PMA_ALLOW_IPS}"
|
||||||
|
cat > /etc/apache2/conf-available/phpmyadmin-restrict.conf <<EOF
|
||||||
|
<Directory /usr/share/phpmyadmin>
|
||||||
|
<RequireAny>
|
||||||
|
${ALLOW_BLOCK}
|
||||||
|
</RequireAny>
|
||||||
|
</Directory>
|
||||||
|
EOF
|
||||||
|
a2enconf -q phpmyadmin-restrict
|
||||||
|
systemctl reload apache2
|
||||||
|
warn "phpMyAdmin reachable only from: ${PMA_ALLOW_IPS}"
|
||||||
|
warn "Remote admin? SSH tunnel: ssh -L 8080:localhost:80 user@server then http://localhost:8080/phpmyadmin"
|
||||||
|
fi
|
||||||
|
|
||||||
|
#─────────────────────────────────────────────────────────────────────────────
|
||||||
|
log "Firewall (UFW)"
|
||||||
|
#─────────────────────────────────────────────────────────────────────────────
|
||||||
|
ufw allow OpenSSH
|
||||||
|
ufw allow 'Apache Full' # 80 + 443
|
||||||
|
ufw --force enable
|
||||||
|
|
||||||
|
#─────────────────────────────────────────────────────────────────────────────
|
||||||
|
log "Fail2ban (SSH brute-force protection)"
|
||||||
|
#─────────────────────────────────────────────────────────────────────────────
|
||||||
|
cat > /etc/fail2ban/jail.local <<'EOF'
|
||||||
|
[sshd]
|
||||||
|
enabled = true
|
||||||
|
maxretry = 5
|
||||||
|
bantime = 1h
|
||||||
|
findtime = 10m
|
||||||
|
EOF
|
||||||
|
systemctl enable --now fail2ban
|
||||||
|
systemctl restart fail2ban
|
||||||
|
|
||||||
|
#─────────────────────────────────────────────────────────────────────────────
|
||||||
|
log "Unattended security upgrades"
|
||||||
|
#─────────────────────────────────────────────────────────────────────────────
|
||||||
|
dpkg-reconfigure -f noninteractive unattended-upgrades || true
|
||||||
|
|
||||||
|
#─────────────────────────────────────────────────────────────────────────────
|
||||||
|
if [[ "$INSTALL_CERTBOT" == "yes" ]]; then
|
||||||
|
log "Certbot (Let's Encrypt TLS)"
|
||||||
|
apt-get install -y certbot python3-certbot-apache
|
||||||
|
warn "Get a cert AFTER DNS points here: certbot --apache -d ${SERVER_NAME} -m ${ADMIN_EMAIL} --agree-tos"
|
||||||
|
fi
|
||||||
|
|
||||||
|
#─────────────────────────────────────────────────────────────────────────────
|
||||||
|
if [[ "$INSTALL_SWAP" == "yes" ]]; then
|
||||||
|
log "Swapfile (RAM safety net)"
|
||||||
|
# Why: when RAM fills, kernel pages cold memory to disk instead of OOM-killing
|
||||||
|
# MySQL/Apache. Disk-slow, but stops crashes on memory spikes (small VPS).
|
||||||
|
if swapon --show | grep -q '/swapfile'; then
|
||||||
|
warn "Swap already active, skipping"
|
||||||
|
else
|
||||||
|
# Size: ~2x RAM if tiny, else = RAM, capped 4G
|
||||||
|
if [[ $RAM_MB -le 2000 ]]; then SWAP_G=$(( (RAM_MB*2+512)/1024 ))
|
||||||
|
elif [[ $RAM_MB -le 8000 ]]; then SWAP_G=$(( (RAM_MB+512)/1024 ))
|
||||||
|
else SWAP_G=4; fi
|
||||||
|
[[ $SWAP_G -lt 1 ]] && SWAP_G=1
|
||||||
|
fallocate -l "${SWAP_G}G" /swapfile || dd if=/dev/zero of=/swapfile bs=1M count=$((SWAP_G*1024))
|
||||||
|
chmod 600 /swapfile
|
||||||
|
mkswap /swapfile
|
||||||
|
swapon /swapfile
|
||||||
|
grep -q '/swapfile' /etc/fstab || echo '/swapfile none swap sw 0 0' >> /etc/fstab
|
||||||
|
# Less aggressive swapping — prefer RAM, swap only when needed (server tuning)
|
||||||
|
sysctl -w vm.swappiness=10
|
||||||
|
grep -q '^vm.swappiness' /etc/sysctl.conf || echo 'vm.swappiness=10' >> /etc/sysctl.conf
|
||||||
|
log "Swap on: ${SWAP_G}G (swappiness=10)"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
#─────────────────────────────────────────────────────────────────────────────
|
||||||
|
if [[ "$TUNE_PHP" == "yes" ]]; then
|
||||||
|
log "Tune PHP — OPcache + FPM pool (sized to RAM)"
|
||||||
|
PHP_INI_DIR="/etc/php/${PHP_VER}/fpm/conf.d"
|
||||||
|
# OPcache: caches compiled PHP bytecode in RAM -> big speed win.
|
||||||
|
cat > "${PHP_INI_DIR}/99-custom-opcache.ini" <<'EOF'
|
||||||
|
; Compiled-script cache. Skips re-parsing PHP on every request.
|
||||||
|
opcache.enable=1
|
||||||
|
opcache.memory_consumption=128 ; MB for cached bytecode
|
||||||
|
opcache.interned_strings_buffer=16
|
||||||
|
opcache.max_accelerated_files=10000 ; raise if many .php files
|
||||||
|
opcache.revalidate_freq=2 ; sec between file-change checks (prod: higher)
|
||||||
|
opcache.fast_shutdown=1
|
||||||
|
; General PHP sanity for web apps
|
||||||
|
upload_max_filesize=64M
|
||||||
|
post_max_size=64M
|
||||||
|
memory_limit=256M
|
||||||
|
max_execution_time=60
|
||||||
|
EOF
|
||||||
|
# FPM pool: how many PHP workers. Each ~30-50MB. Size to half RAM / 40MB.
|
||||||
|
POOL="/etc/php/${PHP_VER}/fpm/pool.d/www.conf"
|
||||||
|
MAXCHILD=$(( RAM_MB / 2 / 40 )); [[ $MAXCHILD -lt 4 ]] && MAXCHILD=4
|
||||||
|
if [[ -f "$POOL" ]]; then
|
||||||
|
sed -i \
|
||||||
|
-e "s/^pm.max_children = .*/pm.max_children = ${MAXCHILD}/" \
|
||||||
|
-e "s/^pm.start_servers = .*/pm.start_servers = $(( MAXCHILD/4>0?MAXCHILD/4:1 ))/" \
|
||||||
|
-e "s/^pm.min_spare_servers = .*/pm.min_spare_servers = $(( MAXCHILD/4>0?MAXCHILD/4:1 ))/" \
|
||||||
|
-e "s/^pm.max_spare_servers = .*/pm.max_spare_servers = $(( MAXCHILD/2>0?MAXCHILD/2:2 ))/" \
|
||||||
|
"$POOL"
|
||||||
|
log "FPM pm.max_children=${MAXCHILD}"
|
||||||
|
fi
|
||||||
|
systemctl restart php${PHP_VER}-fpm
|
||||||
|
fi
|
||||||
|
|
||||||
|
#─────────────────────────────────────────────────────────────────────────────
|
||||||
|
if [[ "$INSTALL_DB_BACKUP" == "yes" ]]; then
|
||||||
|
log "Nightly DB backup cron"
|
||||||
|
mkdir -p /var/backups/mysql
|
||||||
|
chmod 700 /var/backups/mysql
|
||||||
|
# Dumps every DB to its own gzip, keeps 7 days. Root uses socket auth.
|
||||||
|
cat > /usr/local/bin/db-backup <<'EOF'
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
DEST=/var/backups/mysql
|
||||||
|
STAMP=$(date +%F)
|
||||||
|
for DB in $(mariadb -N -e "SHOW DATABASES" | grep -Ev '^(information_schema|performance_schema|mysql|sys)$'); do
|
||||||
|
mariadb-dump --single-transaction --quick "$DB" | gzip > "${DEST}/${DB}-${STAMP}.sql.gz"
|
||||||
|
done
|
||||||
|
# Rotate: delete dumps older than 7 days
|
||||||
|
find "$DEST" -name '*.sql.gz' -mtime +7 -delete
|
||||||
|
EOF
|
||||||
|
chmod 700 /usr/local/bin/db-backup
|
||||||
|
# Run 03:15 nightly
|
||||||
|
cat > /etc/cron.d/db-backup <<'EOF'
|
||||||
|
15 3 * * * root /usr/local/bin/db-backup >> /var/log/db-backup.log 2>&1
|
||||||
|
EOF
|
||||||
|
log "Backups -> /var/backups/mysql (nightly 03:15, 7-day retention)"
|
||||||
|
warn "Manual run: sudo db-backup"
|
||||||
|
fi
|
||||||
|
|
||||||
|
#─────────────────────────────────────────────────────────────────────────────
|
||||||
|
if [[ "$INSTALL_HEALTHCHECK" == "yes" ]]; then
|
||||||
|
log "Healthcheck command"
|
||||||
|
cat > /usr/local/bin/healthcheck <<EOF
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
# Quick server status. Run: sudo healthcheck
|
||||||
|
echo "=== Services ==="
|
||||||
|
for s in apache2 php${PHP_VER}-fpm mariadb redis-server fail2ban ufw; do
|
||||||
|
if systemctl list-unit-files | grep -q "^\${s}"; then
|
||||||
|
printf "%-18s %s\n" "\$s" "\$(systemctl is-active \$s 2>/dev/null)"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
echo "=== Disk ==="
|
||||||
|
df -h / | awk 'NR==2{print "root "\$5" used ("\$3"/"\$2")"}'
|
||||||
|
echo "=== Memory ==="
|
||||||
|
free -h | awk '/^Mem:/{print "RAM "\$3" / "\$2" used"} /^Swap:/{print "Swap "\$3" / "\$2" used"}'
|
||||||
|
echo "=== Apache config ==="
|
||||||
|
apache2ctl configtest 2>&1
|
||||||
|
echo "=== Listening ports ==="
|
||||||
|
ss -tlnp 2>/dev/null | awk 'NR>1{print \$4}' | sort -u
|
||||||
|
EOF
|
||||||
|
chmod 755 /usr/local/bin/healthcheck
|
||||||
|
log "Run anytime: sudo healthcheck"
|
||||||
|
fi
|
||||||
|
|
||||||
|
#─────────────────────────────────────────────────────────────────────────────
|
||||||
|
if [[ "$HARDEN_SSH" == "yes" ]]; then
|
||||||
|
log "Harden SSH (key-only, no root login)"
|
||||||
|
# SAFETY: only proceed if at least one SSH key exists, else you lock yourself out.
|
||||||
|
KEYFOUND=no
|
||||||
|
for h in /root/.ssh/authorized_keys /home/*/.ssh/authorized_keys; do
|
||||||
|
[[ -s "$h" ]] && KEYFOUND=yes
|
||||||
|
done
|
||||||
|
if [[ "$KEYFOUND" != yes ]]; then
|
||||||
|
warn "NO SSH keys found in any authorized_keys — REFUSING to disable password login."
|
||||||
|
warn "Add your key first: ssh-copy-id user@server then re-run with HARDEN_SSH=yes"
|
||||||
|
else
|
||||||
|
cat > /etc/ssh/sshd_config.d/99-hardening.conf <<'EOF'
|
||||||
|
PermitRootLogin no
|
||||||
|
PasswordAuthentication no
|
||||||
|
ChallengeResponseAuthentication no
|
||||||
|
KbdInteractiveAuthentication no
|
||||||
|
MaxAuthTries 3
|
||||||
|
EOF
|
||||||
|
if sshd -t; then
|
||||||
|
systemctl reload ssh
|
||||||
|
warn "SSH now KEY-ONLY, root login disabled. Keep this session open + test new login before closing!"
|
||||||
|
else
|
||||||
|
rm -f /etc/ssh/sshd_config.d/99-hardening.conf
|
||||||
|
warn "sshd config test failed — hardening reverted, SSH untouched"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
#─────────────────────────────────────────────────────────────────────────────
|
||||||
|
log "Done. Stack ready."
|
||||||
|
#─────────────────────────────────────────────────────────────────────────────
|
||||||
|
cat <<EOF
|
||||||
|
|
||||||
|
Web root : ${WEB_ROOT}
|
||||||
|
Test PHP : http://<server-ip>/info.php (DELETE after check: rm ${WEB_ROOT}/info.php)
|
||||||
|
Apache : $(apache2 -v | head -1 | awk '{print $3}') (event MPM + PHP-FPM)
|
||||||
|
PHP : $(php -v | head -1 | awk '{print $2}')
|
||||||
|
MariaDB : $(mariadb --version | awk '{print $5}' | tr -d ,) -> connect: sudo mariadb
|
||||||
|
$( command -v node >/dev/null && echo " Node : $(node -v)" )
|
||||||
|
$( command -v composer>/dev/null && echo " Composer : $(composer --version | awk '{print $3}')" )
|
||||||
|
$( command -v python3 >/dev/null && echo " Python : $(python3 -V | awk '{print $2}')" )
|
||||||
|
|
||||||
|
.htaccess : ENABLED (mod_rewrite + AllowOverride All)
|
||||||
|
Firewall : UFW active (22/80/443)
|
||||||
|
TLS : certbot --apache -d yourdomain.com
|
||||||
|
|
||||||
|
NEXT:
|
||||||
|
1. rm ${WEB_ROOT}/info.php
|
||||||
|
2. Point DNS to this server
|
||||||
|
3. Run certbot for HTTPS
|
||||||
|
4. Create app DB: sudo mariadb -e "CREATE DATABASE app; CREATE USER 'app'@'localhost' IDENTIFIED BY 'CHANGEME'; GRANT ALL ON app.* TO 'app'@'localhost';"
|
||||||
|
EOF
|
||||||
Reference in New Issue
Block a user