Install and Harden a Debian 12 LAMP Stack on Amazon Lightsail
Building a public-facing web server from a bare operating system image is one of the most effective ways to gain direct control over your hosting environment. Pre-packaged images and managed hosting providers are convenient, but that convenience comes at the cost of control: you are typically locked into specific software versions, patching schedules set by someone else, and configuration choices you cannot change.
This guide takes that trade-off seriously. Most LAMP stack tutorials stop once the software is installed and running. This one does not. Beyond the basic installation, it covers the Apache module configuration choices beyond the defaults, PHP-FPM and why it is preferable to mod_php, MariaDB security hardening, Apache security headers, HTTP/2, SSL/TLS via Let’s Encrypt, and virtual host configuration that enforces HTTPS and www canonicalization. By the end, you will have a server that is not just functional, but defensible.
Why Build from a Bare OS Image Instead of a Pre-Built Stack
Pre-packaged server images, such as Bitnami, bundle the software for you, but they introduce a significant operational problem: upgrading individual components like PHP is not supported in place. When a new PHP version is needed, the entire instance must be replaced and all content migrated to an image built around the new version, assuming that version is even available as an image.
Starting from a Debian OS-only image avoids this entirely. Each component is managed independently through APT, which means PHP, Apache, and MariaDB can each be upgraded on their own schedule without touching the rest of the stack.
Scope
This guide covers building a LAMP stack on Debian 12 (Bookworm) on an Amazon Lightsail instance, starting from the OS-only image. It includes:
- Instance creation and static IP assignment.
- IPv4 firewall hardening and IPv6 evaluation.
- Apache installation, module selection rationale, and security configuration.
- PHP 8.4 installation via the Sury repository with PHP-FPM.
- PHP production and security settings.
- MariaDB installation, security hardening, and least-privilege user setup.
- HTTP/2 and OCSP stapling.
- SSL/TLS certificate provisioning with Certbot and Let’s Encrypt.
- Virtual host configuration for HTTP to HTTPS redirection and non-www to
wwwredirection. DocumentRootownership and permissions.- Brute-force protection with Fail2ban.
- XML-RPC restriction at the virtual host level.
- Rate limiting with
mod_evasive. - Backup, log rotation, and ongoing maintenance guidance.
Amazon Lightsail is used here, but the Linux configuration steps apply to any Debian 12 VPS or server instance from any provider.
Create and Configure the Lightsail Instance
This guide assumes you have an AWS account and access to the AWS Management Console.
Steps 1 through 4 cover Lightsail-specific setup: creating the instance, assigning a static IP, restricting SSH access, and evaluating IPv6. If you are using a different hosting provider, complete the equivalent steps in your provider’s control panel before continuing. From the Install and Configure the LAMP Stack section onward, every command runs identically on any Debian 12 server regardless of provider.
Step 1: Create a Debian 12 OS-Only Instance
Log in to the AWS Management Console and search for Lightsail. Select it from the results.
On the Lightsail home screen, click Create instance.
Review the default Instance location and change it as appropriate.
Under Pick your instance image, select:
- Platform:
Linux/Unix. - Blueprint:
Operating System (OS) onlyfollowed byDebian 12.
Under Choose your instance plan, select the plan type, network type, and size appropriate for your workload. For a low-traffic site or a staging environment, the entry-level plan is sufficient to validate the full configuration.
In the Identify your instance section, enter a unique name for this instance. Leave the Tagging options section unchanged unless your organization requires tagging.
Click Create instance. The instance will show a status of Starting for approximately one minute before transitioning to Running.
Step 2: Attach a Static IP
By default, Lightsail instances are assigned a dynamic public IP address that changes each time the instance is stopped and restarted. A static IP is required before pointing DNS records at the server.
Click the instance name to open its management screen. Click the Networking tab.
In the IPv4 networking section, click Attach static IP.
Provide a unique name for the static IP resource, then click Create and attach. Click Continue when the confirmation message appears.
The static IP address is now displayed in the IPv4 networking section. This is the address you will use in your domain’s DNS A record.
Step 3: Configure the IPv4 Firewall
In this step, restrict SSH access to known IP addresses and confirm that the HTTP and HTTPS ports required for web traffic and SSL certificate provisioning are open.
Unrestricted SSH access on port 22 means every IP address on the internet can attempt to authenticate against your instance. Restricting access to known IP addresses eliminates that attack surface.
In the IPv4 Firewall section, edit the SSH rule. Enable Restrict to IP address and also enable Allow Lightsail browser SSH if you want to retain the ability to connect through the Lightsail console.
Add one or more IP addresses or CIDR ranges that are permitted to connect via SSH. If your IP address changes regularly, consider using a VPN with a fixed egress IP, or maintaining the Lightsail browser SSH option as a fallback.
Click Save.
New Lightsail instances may include rules for HTTP (port 80) and HTTPS (port 443) by default. Confirm both are present in the firewall rule list. Port 80 is required for the Let’s Encrypt HTTP challenge during certificate provisioning. Port 443 serves all HTTPS traffic. If either rule is missing, add it before continuing.
NOTE: This guide relies on the Lightsail network firewall and does not install UFW. Running both is possible but increases administrative overhead without significant benefit for most Lightsail deployments.
Optional: Disable SSH Password Authentication
After verifying that SSH key access works, disable password authentication to prevent brute-force attempts against the SSH service. Open the SSH daemon configuration file:
sudo nano /etc/ssh/sshd_config
Locate and set the following directives:
PasswordAuthentication no
PermitRootLogin no
Restart SSH to apply the changes:
sudo systemctl restart ssh
Step 4: Evaluate IPv6 Requirements
Many modern hosting environments support IPv6 by default, and there is nothing inherently insecure about leaving it enabled. The important consideration is whether you intend to manage and monitor IPv6 connectivity alongside IPv4.
If you do not plan to use IPv6, disabling it reduces the network surface you must maintain. If you expect to serve IPv6 traffic, leave it enabled and ensure firewall rules, DNS records, and monitoring cover both protocols.
In the IPv6 networking section, either leave IPv6 enabled or disable it based on your operational requirements.
Connect to the Instance
At this point, the instance is running with a static IP and a restricted SSH firewall rule.
From the Lightsail home screen, click the instance name and then click the Connect tab.
NOTE: Two connection methods are available. The browser-based SSH client is sufficient for the steps in this guide. For ongoing administration, a dedicated SSH client such as PuTTY (Windows) or a terminal with an SSH key configured is more capable.
Click Connect using SSH to open a browser-based terminal session.
If you are using a different hosting provider, connect using your provider’s browser-based console or an SSH client pointed at your server’s public IP address. The commands from this point forward are identical regardless of how you connect.
Check Baseline Disk Usage
Before installing anything, record the current disk usage. This provides a baseline for tracking how much space the LAMP stack consumes and helps identify unexpected disk growth later.
df -h
The output shows used and available space on each mounted filesystem. Note the values for / before proceeding.
Update Installed Packages
Update the local APT package index and upgrade all installed packages before adding anything new. Running upgrades on a fresh instance ensures you are not installing software on top of an unpatched OS.
sudo apt update && sudo apt upgrade -y
Review the output for any notices about a required reboot. If prompted, reboot before continuing:
sudo reboot
Reconnect via SSH after the instance restarts.
Install Apache on Debian
Installation
sudo apt install -y apache2
Apache starts automatically after installation. Verify that it is running:
sudo systemctl status apache2
NOTE: Press
qto return to the prompt.
Configure Apache Modules for HTTP/2 and PHP-FPM
The default Apache installation enables a set of modules, but it does not necessarily enable the ones you need, and it may include some you do not. Every enabled module that is not required expands the attack surface of the server.
List all currently loaded modules:
/usr/sbin/apache2ctl -M
Review the output against the modules below. The following changes are recommended for a secure, performant server that will run PHP applications.
- Disable
mpm_prefork: The prefork MPM handles each connection in a separate process. It is incompatible with HTTP/2 and is less efficient than the event MPM for most workloads. - Enable
mpm_event: The event MPM uses a thread-based model that handles concurrent connections more efficiently and is required for HTTP/2 support. - Enable
expires: GeneratesExpiresandCache-ControlHTTP headers, which control browser and proxy caching behavior. - Enable
headers: Allows modification of HTTP request and response headers. Required for setting security headers such as HSTS. - Enable
http2: Enables the HTTP/2 protocol, which provides multiplexing, header compression, and improved performance over HTTPS compared to HTTP/1.1. Requiresmpm_event. - Enable
proxyandproxy_fcgi: These modules enable the FastCGI reverse proxy, which is required to pass PHP requests to PHP-FPM. - Enable
rewrite: Enables rule-based URL rewriting. Required by WordPress and many other PHP applications running on Apache. - Enable
ssl: Adds support for TLS (HTTPS). Required for HTTPS virtual hosts.
Execute the following commands:
sudo a2dismod mpm_prefork
sudo a2enmod mpm_event
sudo a2enmod expires
sudo a2enmod headers
sudo a2enmod http2
sudo a2enmod proxy
sudo a2enmod proxy_fcgi
sudo a2enmod rewrite
sudo a2enmod ssl
Restart Apache to activate the module changes:
sudo apache2ctl configtest
sudo systemctl restart apache2
Install PHP 8.4 With PHP-FPM
Why PHP 8.4
Debian 12 ships PHP 8.2 in its default repositories. PHP 8.2 is still receiving security-only patches, but active bug fix support ended December 31, 2024, and the version reaches full end of life December 31, 2026. PHP 8.4 is a strong choice for new deployments in 2026: it is in active support through December 31, 2028, and offers performance improvements and property hooks over 8.3. PHP 8.5 was released in November 2025 but is still gaining ecosystem momentum; PHP 8.4 is the more broadly compatible choice today for WordPress and most PHP applications.
Why PHP-FPM Instead of mod_php
PHP-FPM (FastCGI Process Manager) runs as a separate service from Apache. This architecture offers several advantages over the traditional mod_php approach where PHP runs inside the Apache process:
- PHP worker processes are isolated from Apache, so a PHP crash does not take down the web server.
- PHP-FPM pools can be configured independently per virtual host with their own user, process count, and resource limits.
- Memory consumed by PHP is not held inside Apache worker processes, which reduces overall memory pressure.
- PHP-FPM is required for HTTP/2 to work correctly with PHP, since
mod_phpis incompatible with the threaded MPMs needed for HTTP/2.
Add the Sury PHP Repository
At the time of writing, Debian 12 does not include PHP 8.4 in its default repositories. The Sury repository provides PHP 8.4 packages for Debian. It is the standard and well-established source for non-default PHP versions on Debian.
Install the required supporting packages first:
sudo apt install -y ca-certificates curl lsb-release
Download and install the Sury keyring package. This places the repository signing key in /usr/share/keyrings/ using the current recommended approach rather than the deprecated apt-key workflow:
sudo curl -sSLo /tmp/debsuryorg-archive-keyring.deb https://packages.sury.org/debsuryorg-archive-keyring.deb
sudo dpkg -i /tmp/debsuryorg-archive-keyring.deb
Add the Sury PHP repository to APT’s source list:
sudo sh -c 'echo "deb [signed-by=/usr/share/keyrings/deb.sury.org-php.gpg] https://packages.sury.org/php/ $(lsb_release -sc) main" > /etc/apt/sources.list.d/php.list'
Update the package index to make APT aware of the new repository:
sudo apt update && sudo apt upgrade -y
Install PHP 8.4 and Extensions
Install PHP 8.4 with PHP-FPM and the extensions most commonly required by PHP applications including WordPress:
sudo apt install -y php8.4 php8.4-{common,fpm,mysql,curl,igbinary,imagick,intl,mbstring,xml,zip}
NOTE: Many PHP install commands found online append
libapache2-mod-php8.4to this command. That package installsmod_php, the traditional method of running PHP inside Apache processes. Since this guide uses PHP-FPM exclusively,libapache2-mod-php8.4is not installed. Some guides install it and then immediately disable it witha2dismod, which achieves the same result but installs an unnecessary package.
The extensions in this command serve the following purposes:
fpm: the FastCGI Process Manager service.mysql: PDO and MySQLi database drivers.curl: HTTP client library used by many plugins and APIs.igbinary: a compact binary serialization format, often used with caching systems.imagick: ImageMagick bindings for image processing.intl: internationalization support.mbstring: multibyte string handling, required by many frameworks.xml: XML parsing, required by WordPress and most CMS platforms.zip: ZIP archive support, required for plugin and theme installation in WordPress.
The php8.4-imagick package pulls in the required ImageMagick system libraries as dependencies automatically. Verify they installed correctly. Debian 12 ships ImageMagick 6, which uses the convert command:
convert --version
If the command is not found, install the underlying package directly:
sudo apt install -y imagemagick
Configure PHP-FPM for Production
The default PHP-FPM configuration is conservative in ways that cause common operational problems, particularly for WordPress. The PHP-FPM configuration file for PHP 8.4 is located at /etc/php/8.4/fpm/php.ini. Open it:
sudo nano /etc/php/8.4/fpm/php.ini
The file is several hundred lines long. In nano, use Ctrl+W to search for each setting by name rather than scrolling manually. Alternatively, use sed to update each value directly from the command line without opening the file:
sudo sed -i 's/^memory_limit = .*/memory_limit = 256M/' /etc/php/8.4/fpm/php.ini
sudo sed -i 's/^upload_max_filesize = .*/upload_max_filesize = 64M/' /etc/php/8.4/fpm/php.ini
sudo sed -i 's/^post_max_size = .*/post_max_size = 64M/' /etc/php/8.4/fpm/php.ini
sudo sed -i 's/^max_execution_time = .*/max_execution_time = 120/' /etc/php/8.4/fpm/php.ini
Review and adjust the following four settings, which are the most common sources of issues after a fresh installation:
| Setting | Default | Recommended |
|---|---|---|
memory_limit | 128M | 256M |
upload_max_filesize | 2M | 64M |
post_max_size | 8M | 64M |
max_execution_time | 30 | 120 |
memory_limitcontrols how much memory a single PHP process can consume. WordPress with active plugins frequently exceeds128M.upload_max_filesizeandpost_max_sizecontrol the maximum size of uploaded files; the default2Mlimit will reject most image uploads. Note thatpost_max_sizemust be equal to or greater thanupload_max_filesize.max_execution_timesets the maximum number of seconds a PHP script can run before it is terminated;30seconds is often too short for plugin installations or import operations.
Two security-oriented settings are also worth applying while the file is open:
sudo sed -i 's/^expose_php = .*/expose_php = Off/' /etc/php/8.4/fpm/php.ini
expose_php = Off prevents PHP from advertising its version in the X-Powered-By HTTP response header, reducing fingerprinting. This setting has no compatibility implications and is safe to apply universally.
After saving changes to php.ini, restart PHP-FPM for the new values to take effect:
sudo systemctl restart php8.4-fpm
Optional: Restrict Shell Execution Functions
disable_functions blocks PHP functions that execute OS-level shell commands. When a vulnerable application allows arbitrary code execution, these functions are what an attacker uses to run commands on the server. Restricting them can limit the impact of a compromise.
NOTE: This setting is not universally safe to apply without testing. Backup tools, deployment utilities, image-processing integrations, PDF generators, and some enterprise plugins legitimately use one or more of these functions. Applying
disable_functionswithout verifying compatibility first can break application functionality in ways that are not immediately obvious.If you choose to apply this setting, test your full plugin stack thoroughly before deploying to a live site. If something breaks, check the PHP error log to identify which function was called, then remove only that specific function from the list rather than disabling the directive entirely.
sudo sed -i 's/^disable_functions = .*/disable_functions = exec,passthru,shell_exec,system,proc_open,popen/' /etc/php/8.4/fpm/php.ini
After any changes to php.ini, restart PHP-FPM:
sudo systemctl restart php8.4-fpm
Enable PHP-FPM With Apache
Enable the FastCGI proxy configuration and the PHP 8.4 FPM configuration for Apache:
sudo a2enmod proxy_fcgi setenvif
sudo a2enconf php8.4-fpm
Enable the PHP-FPM service so it starts automatically at boot:
sudo systemctl enable php8.4-fpm
Restart both services to apply the configuration. Restart PHP-FPM first so the socket is ready before Apache attempts to connect to it:
sudo systemctl restart php8.4-fpm
sudo apache2ctl configtest
sudo systemctl restart apache2
Verify that PHP-FPM is running:
sudo systemctl status php8.4-fpm
Also confirm the PHP-FPM socket file exists at the path Apache expects:
ls /run/php/php8.4-fpm.sock
NOTE: If this command returns
No such file or directory, PHP-FPM is not running or failed to start. Runsudo systemctl status php8.4-fpmto see the reason. A missing socket is the most common cause of Apache returning a 503 error after following this guide; the Apache error log will showAH02454: FCGI: attempt to connect to Unix domain socket /run/php/php8.4-fpm.sock failedwhen this happens.
Install and Secure MariaDB
MariaDB is an open-source fork of MySQL maintained by the original MySQL developers after MySQL was acquired by Oracle. It is a drop-in replacement for MySQL and is fully compatible with PHP’s pdo_mysql and mysqli extensions.
Update packages and install MariaDB:
sudo apt update && sudo apt upgrade -y
sudo apt install -y mariadb-server
Secure the MariaDB Installation
MariaDB ships with several default settings that are appropriate for initial installation but should be changed before the server is accessible to applications. The mysql_secure_installation script walks through each of them:
sudo mariadb-secure-installation
When prompted, use the following responses:
| Prompt | Recommended Response |
|---|---|
| Enter current password for root | Press Enter (no password set yet) |
Switch to unix_socket authentication | Y (on Debian 12, accept the default unix_socket authentication and set a root password as an additional authentication method) |
| Change the root password | Y (if prompted) |
| Remove anonymous users | Y |
| Disallow root login remotely | Y |
| Remove test database | Y |
| Reload privilege tables | Y |
Depending on the MariaDB version and package configuration, the root account may continue using unix_socket authentication exclusively. This is normal on Debian and does not reduce security.
NOTE: On Debian 12, MariaDB uses
unix_socketauthentication for therootaccount by default, allowing the operating systemrootuser to connect without a password. AcceptingYpreserves this behavior while applying the remaining security recommendations from the script.
Restart MariaDB to ensure all configuration changes are active:
sudo systemctl restart mariadb
NOTE: On Debian 12 the service is named
mariadb, notmysql. If you encounter references in older guides or scripts that usesudo systemctl restart mysql, substitutemariadbinstead.
Create an Application Database and User
Do not use the MariaDB root account for application connections. Create a dedicated database and user for each application you host.
Access the MariaDB shell using either of the following commands. Both are valid after the steps above:
sudo mariadb
sudo mariadb -u root -p
Inside the MariaDB shell:
CREATE DATABASE example_db CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
CREATE USER 'example_user'@'localhost' IDENTIFIED BY 'strong_password_here';
GRANT ALL PRIVILEGES ON example_db.* TO 'example_user'@'localhost';
FLUSH PRIVILEGES;
EXIT;
GRANT ALL PRIVILEGES on a single database is appropriate for initial setup and for running migrations or schema changes. For tighter production hardening, consider splitting this into two users: a runtime user granted only SELECT, INSERT, UPDATE, DELETE for day-to-day application queries, and a migration user with fuller privileges used only during deployments. Most CMS platforms including WordPress operate correctly with the restricted runtime grant once the schema is in place.
Using utf8mb4 with utf8mb4_unicode_ci is the correct character set for modern applications. The older utf8 in MySQL/MariaDB is a misnomer that only supports three-byte Unicode characters; utf8mb4 is the full four-byte implementation that supports emoji and supplemental characters.
Harden Apache Security and Enable HTTP/2
Suppress Server Version Information
By default, Apache includes its version number and OS details in HTTP response headers and in server-generated error pages. This information does not need to be public and can help an attacker identify which vulnerabilities apply to the server.
Create a new configuration file:
sudo nano /etc/apache2/conf-available/custom.conf
Add the following content:
# Suppress version and OS information from response headers
ServerTokens Prod
ServerSignature Off
# Exclude inode number from ETag to prevent filesystem information disclosure
FileETag MTime Size
# Enable HTTP/2 over TLS (h2) with HTTP/1.1 fallback
<IfModule mod_http2.c>
Protocols h2 http/1.1
</IfModule>
# OCSP stapling cache for TLS handshake performance
<IfModule mod_ssl.c>
SSLStaplingCache "shmcb:logs/ssl_stapling(32768)"
</IfModule>
ServerTokens Prod limits the Server response header to just Apache, removing the version number. ServerSignature Off removes the server footer from error pages. FileETag MTime Size generates ETags using the file’s last-modified time and size, excluding the inode number. The default FileETag INode MTime Size includes the inode, which can leak internal filesystem information and causes cache mismatches in multi-server environments. Removing the inode component retains ETag-based cache validation while eliminating that disclosure. These directives operate at the server configuration level and apply globally across all virtual hosts.
NOTE: Many guides include
h2cin theProtocolsdirective alongsideh2.h2cis HTTP/2 over unencrypted TCP and has no practical use on a public HTTPS server. It is omitted here intentionally.
Update the security.conf File
Open the existing security configuration file:
sudo nano /etc/apache2/conf-available/security.conf
Comment out the existing ServerTokens and ServerSignature directives so they do not conflict with the values set in custom.conf. Confirm that TraceEnable is set to Off:
# ServerTokens Minimal
# ServerTokens OS
# ServerTokens Full
# ServerSignature Off
# ServerSignature On
TraceEnable Off
The HTTP TRACE method is used for diagnostic purposes and enables cross-site tracing (XST) attacks in some configurations. Disabling it is standard practice for any production server.
Deny Access to the Default Virtual Host
Apache’s default virtual host responds to all HTTP requests on port 80 that do not match a named virtual host. Accessing the server’s IP address directly in a browser will serve the default Apache welcome page, which discloses that Apache is installed on the server.
Edit the default host configuration to deny all access:
sudo nano /etc/apache2/sites-available/000-default.conf
Replace the file contents with:
<VirtualHost _default_:80>
DocumentRoot /var/www/html
ErrorLog ${APACHE_LOG_DIR}/error.log
CustomLog ${APACHE_LOG_DIR}/access.log combined
<Location />
Require all denied
Options None
AllowOverride None
</Location>
</VirtualHost>
Define a Virtual Host for HTTP
Create a virtual host configuration for your domain. In this example, replace example.com with your actual domain name.
sudo nano /etc/apache2/sites-available/example-com.conf
<VirtualHost _default_:80>
ServerName example.com
ServerAlias www.example.com
DocumentRoot /www/example-com/public_html
<Directory "/www/example-com/public_html">
Options FollowSymLinks
AllowOverride None
Require all granted
</Directory>
ErrorLog ${APACHE_LOG_DIR}/error.log
CustomLog ${APACHE_LOG_DIR}/access.log combined
<IfModule mod_rewrite.c>
# Redirect all HTTP requests to HTTPS
RewriteEngine On
RewriteCond %{HTTPS} !=on
RewriteRule ^ https://%{SERVER_NAME}%{REQUEST_URI} [NE,R,L]
</IfModule>
</VirtualHost>
The AllowOverride None directive at this stage is intentional. .htaccess file processing carries a performance cost because Apache must check for the file in every directory it traverses to serve a request. Since this virtual host immediately redirects all traffic to HTTPS without serving any content, .htaccess processing here would add overhead with no benefit. AllowOverride All is set in the HTTPS virtual host in a later step, where application rules actually need to take effect.
Canonical URL Preference
This guide uses www.example.com as the canonical domain, meaning all requests to example.com are permanently redirected to www.example.com. If you prefer example.com as canonical, the HTTP virtual host above requires no changes, as it redirects everything to HTTPS regardless of the subdomain. The canonicalization choice only affects the rewrite rule in the HTTPS virtual host, which is covered in a later step.
Create the DocumentRoot and Set File Permissions
The DocumentRoot referenced in the virtual host configuration does not exist yet. The path /www/example-com/public_html is used here to keep application content cleanly separated from the default Apache directory tree under /var/www/. Either location works as long as the virtual host configuration matches; adjust the path to suit your preference before running the commands below.
Create the directory:
sudo mkdir -p /www/example-com/public_html
Set ownership to the www-data user and group that Apache runs under:
sudo chown -R www-data:www-data /www/example-com/public_html
Set standard permissions: 755 for directories and 644 for files:
sudo find /www/example-com/public_html -type d -exec chmod 755 {} \;
sudo find /www/example-com/public_html -type f -exec chmod 644 {} \;
These find commands are safe to run on an empty DocumentRoot at setup time. If you run them again later on a live WordPress installation, they will correctly reset permissions on existing content, but any directories created by WordPress during that interval (such as dated upload folders under wp-content/uploads/) will already carry the correct ownership from the www-data process that created them. The commands do not break anything when re-run; just be aware they operate on the state of the filesystem at the moment they execute.
Enable the New Configurations
sudo a2enconf custom.conf
sudo a2ensite 000-default.conf
sudo a2ensite example-com.conf
sudo apache2ctl configtest
sudo systemctl reload apache2
NOTE: The
000-default.confsite is already enabled by default on a fresh Apache installation, soa2ensite 000-default.confwill outputSite 000-default already enabled. This is expected and not an error; the command is included to ensure the updated configuration is active if the file was previously disabled.
reload is used here rather than restart because it sends a graceful signal that re-reads the configuration without dropping any active connections. During the setup phase the difference is academic since no traffic is flowing, but using reload where a full restart is not required is the correct habit for a live server.
Install Certbot and Provision a Let’s Encrypt SSL Certificate
Let’s Encrypt provides free, automatically renewed SSL/TLS certificates. Certbot is the official client for Let’s Encrypt and integrates directly with Apache to configure HTTPS virtual hosts.
NOTE: A domain name pointed at your server’s static IP address is required to complete this section. Let’s Encrypt cannot issue certificates for bare IP addresses. If you want to verify that Apache, PHP-FPM, and MariaDB are working before configuring DNS, skip ahead to the Verify the LAMP Stack section and test over HTTP using the server’s IP address directly. Temporarily remove the HTTP-to-HTTPS rewrite rule from the virtual host configuration to allow direct HTTP access, then return here once DNS is in place.
Before running Certbot, the DNS A record for your domain must already point to the static IP address of the Lightsail instance. Let’s Encrypt verifies domain ownership by making an HTTP request to the domain as part of the certificate issuance process. If DNS has not propagated yet, the challenge will fail.
Install Certbot via Snap
Remove any previous APT-based Certbot installation to avoid conflicts:
sudo apt remove certbot
Install snapd and ensure the service is fully running before issuing snap commands. On some Lightsail instances, snapd does not start automatically after the apt install and snap commands will fail with a socket error if you proceed immediately:
sudo apt install snapd -y
sudo systemctl enable --now snapd
Install and refresh the core snap. On a fresh install these two commands are run together: the first installs core if it is not present, the second ensures it is current:
sudo snap install core; sudo snap refresh core
Install Certbot:
sudo snap install --classic certbot
Create a symbolic link so the certbot command is available system-wide:
sudo ln -sf /snap/bin/certbot /usr/bin/certbot
Request the Certificate
Run Certbot to request a certificate covering both domains:
sudo certbot --apache -d example.com -d www.example.com
The first -d argument determines the directory name created under /etc/letsencrypt/live/ (conventionally the bare domain, regardless of which form is canonical). The canonical redirect direction is a separate decision handled by the rewrite rule in the HTTPS virtual host, not by the order of -d arguments here.
This creates a certificate covering both example.com and www.example.com, with the live files stored under /etc/letsencrypt/live/example.com/. The rewrite rule configured in the next step determines which form is treated as canonical.
Certbot will prompt for an e-mail address for expiry notifications and confirmation of the Let’s Encrypt Terms of Service. After completing the prompts, Certbot verifies ownership of the domain and issues the certificate.
Let’s Encrypt certificates are valid for 90 days. Certbot configures a systemd timer to automatically renew certificates before they expire. Renewal is tied to the domain name, not the IP address: if the static IP is ever detached or DNS is not kept pointing at the server, the renewal HTTP challenge will fail. The e-mail address provided during the Certbot setup prompt is how Let’s Encrypt delivers expiry and renewal failure notifications, so ensure it is a monitored address. Test the renewal process to confirm it is working:
sudo certbot renew --dry-run
Verify the renewal timer is active:
systemctl list-timers | grep certbot
Configure the HTTPS Virtual Host With HSTS and OCSP Stapling
After Certbot issues the certificate, it creates a new virtual host configuration file at /etc/apache2/sites-available/example-com-le-ssl.conf. Edit this file to add the remaining security and redirect configuration:
sudo nano /etc/apache2/sites-available/example-com-le-ssl.conf
<IfModule mod_ssl.c>
<VirtualHost _default_:443>
ServerName example.com
ServerAlias www.example.com
DocumentRoot /www/example-com/public_html
<Directory "/www/example-com/public_html">
Options FollowSymLinks
AllowOverride All
Require all granted
</Directory>
ErrorLog ${APACHE_LOG_DIR}/error.log
CustomLog ${APACHE_LOG_DIR}/access.log combined
# Redirect non-www requests to www
<IfModule mod_rewrite.c>
RewriteEngine On
RewriteCond %{HTTP_HOST} !^www\. [NC]
RewriteRule ^(.*)$ https://www.%{HTTP_HOST}%{REQUEST_URI} [R=permanent,L]
</IfModule>
Include /etc/letsencrypt/options-ssl-apache.conf
SSLCertificateFile /etc/letsencrypt/live/example.com/fullchain.pem
SSLCertificateKeyFile /etc/letsencrypt/live/example.com/privkey.pem
# Enable OCSP stapling for faster TLS handshakes
SSLUseStapling on
# HTTP Strict Transport Security (2 year max-age)
# includeSubDomains applies HSTS to all subdomains; remove if any subdomains are not HTTPS-capable
<IfModule mod_headers.c>
Header always set Strict-Transport-Security "max-age=63072000; includeSubDomains"
</IfModule>
</VirtualHost>
</IfModule>
The rewrite block above redirects bare domain requests to www. If you prefer the bare domain as canonical, replace this block with the following:
# Redirect www requests to non-www
<IfModule mod_rewrite.c>
RewriteEngine On
RewriteCond %{HTTP_HOST} ^www\.(.+)$ [NC]
RewriteRule ^ https://%1%{REQUEST_URI} [R=permanent,L]
</IfModule>
The %1 back-reference captures the domain without the www. prefix from the RewriteCond match and uses it to build the redirect target. Choose one canonical form (www or non-www) and apply it consistently across your DNS records, virtual host configuration, WordPress Site URL setting, and any sitemaps or canonical tags in your theme. See the Canonical URL Preference section above for more detail.
AllowOverride All
This directive activates .htaccess file processing for the DocumentRoot. Without it, Apache ignores any .htaccess files in that directory tree entirely. The trade-off is a small per-request overhead that is negligible on a single-site server. The server-level directives in custom.conf suppress version information globally but do not provide directory-level protections; a hardened .htaccess file extends the security posture further down the stack. Read Harden WordPress Security Using Custom .htaccess Files for a complete set of directory-level protections. If your application does not require .htaccess at all, AllowOverride None with configuration moved into the virtual host is preferable. If you want to scope the override more precisely than All, see the note below.
NOTE:
AllowOverride Allis broader than strictly necessary for most WordPress installations. A more precise alternative isAllowOverride FileInfo Options Indexes AuthConfig Limit, which covers URL rewriting, header modifications, directory options, and the access control directives used by the companion.htaccessguide; everything WordPress requires without leaving the directive fully open.
OCSP Stapling
During a normal TLS handshake, the client must contact the certificate authority to verify that the certificate has not been revoked. This adds latency to every new connection. OCSP stapling moves this step to the server: Apache periodically fetches a signed OCSP response from Let’s Encrypt and includes it in the TLS handshake, eliminating the client-side round trip. This relies on the SSLStaplingCache directive configured earlier in custom.conf. While some browsers now rely less heavily on OCSP checks, stapling remains a recommended TLS optimization and is still beneficial for clients that support it.
HSTS
The Strict-Transport-Security header instructs browsers to only connect to this domain over HTTPS for the duration specified in max-age. Once a browser has received this header, it will refuse to make unencrypted connections to the domain for two years. The includeSubDomains directive extends this enforcement to all subdomains; remove it if any subdomains on this domain are not served over HTTPS. Browser preload lists require includeSubDomains, preload, max-age of at least one year, and HTTPS on the apex domain.
If you intend to submit the domain to browser HSTS preload lists, add the preload directive:
Header always set Strict-Transport-Security "max-age=63072000; includeSubDomains; preload"
Adding preload hardcodes HTTPS enforcement in browsers before the first visit. Only add preload after verifying that every current and future subdomain can be served exclusively over HTTPS. Once accepted into browser preload lists, removal can take months and may temporarily break access to subdomains that are not HTTPS-capable.
Enable the new SSL virtual host and reload Apache:
sudo a2ensite example-com-le-ssl.conf
sudo apache2ctl configtest
sudo systemctl reload apache2
Verify the LAMP Stack
With all components in place, run a final verification pass.
Apache Configuration Syntax Check
sudo apache2ctl configtest
The output should end with Syntax OK.
PHP-FPM Status
sudo systemctl status php8.4-fpm
MariaDB Status
sudo systemctl status mariadb
Verify PHP Version and Active Configuration
php -v
Confirm PHP-FPM is being used by Apache (not mod_php) by creating a temporary info file:
echo "<?php phpinfo(); ?>" | sudo tee /www/example-com/public_html/phpinfo.php
Navigate to https://www.example.com/phpinfo.php in a browser and confirm that the Server API line reads FPM/FastCGI. Remove the file immediately after confirming:
sudo rm /www/example-com/public_html/phpinfo.php
Testing Without DNS or HTTPS
If you have not yet configured DNS or provisioned an SSL certificate, you can verify that Apache is serving PHP correctly by testing over HTTP using localhost directly on the server. First, temporarily add localhost as a ServerAlias in /etc/apache2/sites-available/example-com.conf and remove the HTTP-to-HTTPS rewrite rule from the same file. Reload Apache with sudo apache2ctl configtest && sudo systemctl reload apache2. Then run the following from the server command line:
curl http://localhost/phpinfo.php
Verify the Server API is FPM/FastCGI in the output to confirm PHP-FPM is active.
curl -s http://localhost/phpinfo.php | grep "Server API"
When finished, remove the localhost alias, restore the rewrite rule, and reload Apache again before continuing with the Certbot section.
Final Disk Usage Comparison
df -h
Compare this output to the baseline recorded at the start of the guide. At minimum, it serves as a reference for identifying whether logs or other maintenance activities are growing out of control and unnecessarily consuming storage.
Results
At this point the server is running Debian 12 with Apache, PHP 8.4 via PHP-FPM, and MariaDB. HTTP requests are redirected to HTTPS. HTTPS connections benefit from HTTP/2, OCSP stapling, and HSTS. Server version information is suppressed from response headers and error pages. The default virtual host denies all direct IP access. MariaDB anonymous users, the test database, and remote root login are all removed. PHP production settings for memory, upload size, and execution time have been adjusted from their conservative defaults, and PHP version fingerprinting has been suppressed.
The core platform is now production-ready, but additional security hardening and operational work remains before the server is exposed to public traffic. The following section covers both.
Additional Considerations
The steps in this section fall into two groups. The security hardening items should be completed before the server goes live. The operations items establish the ongoing practices needed to keep it running safely.
Security Hardening
Brute-Force Protection With Fail2ban
WordPress login pages and XML-RPC endpoints are subjected to constant automated brute-force attempts regardless of how small or new a site is. Restricting wp-login.php by IP in .htaccess is the strongest defense, but Fail2ban provides a complementary layer that monitors Apache logs and temporarily bans IPs that generate excessive failed requests.
An important distinction: the four Apache jails configured below operate at the server level, monitoring Apache error and access logs for patterns such as authentication failures, known bad bot signatures, requests for non-existent scripts, and request overflow attacks. They do not natively detect WordPress login failures, because WordPress processes those at the application level and returns an HTTP 200 response rather than an Apache-level authentication error. For WordPress-specific login protection, IP restriction via .htaccess or a dedicated WordPress Fail2ban filter is required. The jails here still provide meaningful protection against a broad class of automated scanning and server-level attacks.
Install Fail2ban:
sudo apt install -y fail2ban
Create a local override configuration file. Fail2ban uses a jail.local file to override defaults without modifying the packaged jail.conf, which may be overwritten on upgrades:
sudo nano /etc/fail2ban/jail.local
Add the following configuration:
[DEFAULT]
bantime = 3600
findtime = 600
maxretry = 5
[apache-auth]
enabled = true
[apache-badbots]
enabled = true
[apache-noscript]
enabled = true
[apache-overflows]
enabled = true
This configuration bans an IP for one hour after five failed attempts within a ten-minute window, and enables four Apache-specific filters that catch authentication failures, known bad bots, requests for non-existent scripts, and request overflow attacks.
Enable and start Fail2ban:
sudo systemctl enable fail2ban
sudo systemctl start fail2ban
Verify that the jails are active:
sudo fail2ban-client status
For WordPress specifically, Fail2ban works from the Apache error and access logs. Ensure Apache is writing logs to the default ${APACHE_LOG_DIR} paths so Fail2ban’s filters can locate them.
Restrict or Disable XML-RPC
WordPress’s xmlrpc.php endpoint enables remote publishing and certain plugin integrations, but it is also a persistent target for brute-force amplification attacks; a single request to XML-RPC can trigger hundreds of authentication attempts. For most small WordPress sites that do not use the Jetpack plugin or remote publishing clients, XML-RPC can be blocked entirely at the Apache virtual host level.
Add the following Location block inside the HTTPS virtual host configuration in /etc/apache2/sites-available/example-com-le-ssl.conf, within the <VirtualHost> directive:
# Block XML-RPC entirely; remove if Jetpack or remote publishing is required
<Location /xmlrpc.php>
Require all denied
</Location>
If XML-RPC access is needed for specific IPs (for example, a Jetpack connection), use RequireAny to restrict rather than block entirely:
<Location /xmlrpc.php>
<RequireAny>
Require ip 203.0.113.1
</RequireAny>
</Location>
Replace 203.0.113.1 with the actual public IP address that requires XML-RPC access. Multiple Require ip lines can be added for additional addresses.
After editing the virtual host file, reload Apache:
sudo apache2ctl configtest
sudo systemctl reload apache2
NOTE: Additional WordPress hardening measures are covered in the Harden WordPress Security Using Custom .htaccess Files article, including locking
wp-login.phpandxmlrpc.phpby IP at the directory level and blocking PHP execution in thewp-content/uploads/directory (preventing malicious files uploaded through a vulnerable plugin from being executed by the server).
Rate Limiting With mod_evasive
For basic protection against simple HTTP flood attacks, mod_evasive monitors request rates per IP and returns a 403 response when thresholds are exceeded. It is a lightweight option well-suited to a small Lightsail instance where a full WAF would be disproportionate.
sudo apt install -y libapache2-mod-evasive
sudo a2enmod evasive
sudo apache2ctl configtest
sudo systemctl restart apache2
The default configuration is conservative and works without modification for most small sites. The configuration file is at /etc/apache2/mods-available/evasive.conf if tuning is needed.
By default, mod_evasive attempts to send an e-mail notification each time it bans an IP. AWS blocks outbound mail on port 25 by default on Lightsail, and many other hosting providers apply similar restrictions. Unless your server has a configured mail transfer agent and outbound mail is permitted, these notifications cannot be delivered and will generate mail delivery errors in the system log.
Open the configuration file:
sudo nano /etc/apache2/mods-available/evasive.conf
Find the DOSEmailNotify line and comment it out to disable e-mail notifications:
#DOSEmailNotify you@yourdomain.com
For sites that attract more targeted traffic or need more granular control, ModSecurity is the next step up, though its configuration overhead is significant and best suited to higher-risk deployments.
Operations
Log Rotation
Apache on Debian installs a logrotate configuration by default, but logrotate itself may not be present on a minimal OS-only image. Install it if it is not already available:
sudo apt install -y logrotate
This also ensures the Apache logrotate configuration at /etc/logrotate.d/apache2 is registered and active. On a small Lightsail instance with limited disk space, unrotated logs from a busy or misbehaving site can fill the disk quickly. Confirm log rotation is configured correctly:
sudo logrotate --debug /etc/logrotate.d/apache2
The output shows what logrotate would do on its next run without actually rotating anything.
phpMyAdmin
phpMyAdmin is a browser-based MariaDB administration interface. Many managed hosting providers install it by default, but it is not recommended on a self-managed public-facing server. Installing phpMyAdmin on the web server adds a publicly accessible administrative interface that is a frequent target for automated scanning and credential stuffing attacks. If database administration access is needed, HeidiSQL is a capable free alternative that connects to the database server over an SSH tunnel, keeping the administrative interface entirely off the public-facing server.
Backups
A server built from scratch with no backup process is a server that cannot be recovered. At minimum, configure automated backups that copy the database and the DocumentRoot off-instance on a regular schedule. Lightsail manual snapshots are a starting point, but they are not a substitute for file-level backups that can be selectively restored. For a practical implementation, read Automated Linux Web Server Backup Script to Google Drive, which covers a scriptable approach to file and database dumps that can be scheduled via cron.
Ongoing Maintenance
Keep the instance updated. The command used at the start of this guide (sudo apt update && sudo apt upgrade -y) should be run regularly or automated with unattended-upgrades configured for security updates. If you configure unattended-upgrades, note that it only applies automatic updates to repositories explicitly listed in /etc/apt/apt.conf.d/50unattended-upgrades under Unattended-Upgrade::Allowed-Origins. The Sury repository is not included in unattended-upgrades by default, so PHP security updates from Sury will require manual installation unless you add it to the allowed origins configuration.
Monitor PHP version support timelines and plan major version upgrades before the active support window closes.
Summary
Starting from a bare Debian image gives you a LAMP stack built from components you control and can upgrade independently. Apache, PHP, and MariaDB are configured intentionally rather than inherited from a pre-packaged image, making long-term maintenance significantly easier.
The result is a server that goes further than a basic install guide delivers: Apache with minimal exposure, PHP 8.4 via PHP-FPM with production-oriented configuration and optional security hardening, a secured MariaDB instance with a least-privilege application user, SSL/TLS with automatic certificate management, security headers at both the server and directory level, Fail2ban monitoring for brute-force attempts, XML-RPC locked down at the virtual host level, and lightweight flood protection via mod_evasive. The backup and maintenance sections give you the operational foundation to keep the server running long-term, and the log rotation and unattended-upgrades notes close gaps that most comparable guides leave open.