<?xml version="1.0" encoding="UTF-8"?><rss version="2.0"
	xmlns:content="http://purl.org/rss/1.0/modules/content/"
	xmlns:wfw="http://wellformedweb.org/CommentAPI/"
	xmlns:dc="http://purl.org/dc/elements/1.1/"
	xmlns:atom="http://www.w3.org/2005/Atom"
	xmlns:sy="http://purl.org/rss/1.0/modules/syndication/"
	xmlns:slash="http://purl.org/rss/1.0/modules/slash/"
	>

<channel>
	<title>Apache Archives | Abdul Wahab Junaid</title>
	<atom:link href="https://awjunaid.com/category/apache/feed/" rel="self" type="application/rss+xml" />
	<link>https://awjunaid.com/category/apache/</link>
	<description>Offensive Security Researcher &#38; Quantum Cryptography Analyst</description>
	<lastBuildDate>Sun, 26 Jul 2026 14:13:25 +0000</lastBuildDate>
	<language>en-US</language>
	<sy:updatePeriod>
	hourly	</sy:updatePeriod>
	<sy:updateFrequency>
	1	</sy:updateFrequency>
	

<image>
	<url>https://i0.wp.com/awjunaid.com/wp-content/uploads/2023/06/cropped-1668274976669.jpeg?fit=32%2C32&#038;ssl=1</url>
	<title>Apache Archives | Abdul Wahab Junaid</title>
	<link>https://awjunaid.com/category/apache/</link>
	<width>32</width>
	<height>32</height>
</image> 
<site xmlns="com-wordpress:feed-additions:1">220030102</site>	<item>
		<title>How to Set Up Apache for Serving WordPress Websites</title>
		<link>https://awjunaid.com/apache/how-to-set-up-apache-for-serving-wordpress-websites/</link>
					<comments>https://awjunaid.com/apache/how-to-set-up-apache-for-serving-wordpress-websites/?noamp=mobile#respond</comments>
		
		<dc:creator><![CDATA[Abdul Wahab Junaid]]></dc:creator>
		<pubDate>Mon, 11 Sep 2023 11:24:25 +0000</pubDate>
				<category><![CDATA[Apache]]></category>
		<category><![CDATA[apache]]></category>
		<guid isPermaLink="false">https://awjunaid.com/?p=5048</guid>

					<description><![CDATA[<p>WordPress powers a huge chunk of the web, and behind a massive number of those sites sits a&#8230;</p>
<p>The post <a href="https://awjunaid.com/apache/how-to-set-up-apache-for-serving-wordpress-websites/">How to Set Up Apache for Serving WordPress Websites</a> appeared first on <a href="https://awjunaid.com">Abdul Wahab Junaid</a>.</p>
]]></description>
										<content:encoded><![CDATA[
<p class="wp-block-paragraph">WordPress powers a huge chunk of the web, and behind a massive number of those sites sits a quiet workhorse: Apache. If you&#8217;re setting up a new server and want a rock-solid, well-understood platform to host WordPress on, Apache is still one of the best choices out there — not because it&#8217;s flashy, but because it&#8217;s mature, well-documented, and forgiving of mistakes in a way that newer servers sometimes aren&#8217;t.</p>



<h2 class="wp-block-heading">Why Apache for WordPress?</h2>



<p class="wp-block-paragraph">Apache has been the default pairing for WordPress since WordPress existed. A few reasons that pairing has stuck around:</p>



<ul class="wp-block-list">
<li><strong><code>.htaccess</code> support</strong> — WordPress relies heavily on <code>.htaccess</code> files for permalinks, redirects, and security rules. Apache&#8217;s per-directory configuration override system makes this trivial; other servers require workarounds.</li>



<li><strong>Massive ecosystem</strong> — Nearly every hosting tutorial, plugin doc, and Stack Overflow answer assumes Apache.</li>



<li><strong>Module flexibility</strong> — <code>mod_rewrite</code>, <code>mod_ssl</code>, <code>mod_security</code>, and dozens of other modules can be toggled on as needed.</li>



<li><strong>Battle-tested stability</strong> — Apache has been running production workloads for three decades.</li>
</ul>



<p class="wp-block-paragraph">None of this means Apache is objectively &#8220;better&#8221; than Nginx or LiteSpeed — it just means it&#8217;s an extremely safe, well-supported default, especially for people who want config flexibility over raw throughput.</p>



<h2 class="wp-block-heading">Prerequisites</h2>



<p class="wp-block-paragraph">Before starting, make sure you have:</p>



<ul class="wp-block-list">
<li>A server running Ubuntu 22.04/24.04 or a similar Debian-based distro (commands below use <code>apt</code>; adjust for CentOS/RHEL with <code>yum</code>/<code>dnf</code>)</li>



<li>Root or sudo access</li>



<li>A domain name pointed at your server&#8217;s IP (optional for local testing, required for a real launch)</li>



<li>Basic comfort with the command line</li>



<li>MySQL or MariaDB available or installable</li>



<li>PHP 8.1+ (WordPress 6.x recommends PHP 8.0 or newer)</li>
</ul>



<h2 class="wp-block-heading">Step 1: Update the Server and Install Apache</h2>



<pre class="wp-block-code"><code>sudo apt update &amp;&amp; sudo apt upgrade -y
sudo apt install apache2 -y
sudo systemctl enable apache2
sudo systemctl start apache2
</code></pre>



<p class="wp-block-paragraph">Verify it&#8217;s running:</p>



<pre class="wp-block-code"><code>sudo systemctl status apache2
</code></pre>



<p class="wp-block-paragraph">Visit <code>http://your-server-ip</code> in a browser — you should see the default Apache welcome page.</p>



<h2 class="wp-block-heading">Step 2: Install PHP and Required Extensions</h2>



<p class="wp-block-paragraph">WordPress needs PHP along with several extensions for media handling, database access, and caching.</p>



<pre class="wp-block-code"><code>sudo apt install php php-mysql php-curl php-gd php-mbstring \
  php-xml php-xmlrpc php-soap php-intl php-zip libapache2-mod-php -y
</code></pre>



<p class="wp-block-paragraph">Restart Apache to load the PHP module:</p>



<pre class="wp-block-code"><code>sudo systemctl restart apache2
</code></pre>



<p class="wp-block-paragraph">Confirm PHP works by creating a test file:</p>



<pre class="wp-block-code"><code>echo "&lt;?php phpinfo(); ?&gt;" | sudo tee /var/www/html/info.php
</code></pre>



<p class="wp-block-paragraph">Visit <code>http://your-server-ip/info.php</code>, confirm the PHP info page loads, then <strong>delete this file</strong> — leaving it exposed is a common and easily avoidable security mistake.</p>



<pre class="wp-block-code"><code>sudo rm /var/www/html/info.php
</code></pre>



<h2 class="wp-block-heading">Step 3: Install and Secure MySQL/MariaDB</h2>



<pre class="wp-block-code"><code>sudo apt install mysql-server -y
sudo mysql_secure_installation
</code></pre>



<p class="wp-block-paragraph">Answer the prompts to set a root password, remove anonymous users, disable remote root login, and remove the test database.</p>



<p class="wp-block-paragraph">Create a dedicated database and user for WordPress:</p>



<pre class="wp-block-code"><code>sudo mysql -u root -p
</code></pre>



<pre class="wp-block-code"><code>CREATE DATABASE wordpress_db;
CREATE USER 'wp_user'@'localhost' IDENTIFIED BY 'Str0ng-Unique-Password';
GRANT ALL PRIVILEGES ON wordpress_db.* TO 'wp_user'@'localhost';
FLUSH PRIVILEGES;
EXIT;
</code></pre>



<p class="wp-block-paragraph">Never use <code>root</code> as your WordPress database user in production — a compromised WordPress install shouldn&#8217;t automatically mean a compromised database server.</p>



<h2 class="wp-block-heading">Step 4: Download and Configure WordPress</h2>



<pre class="wp-block-code"><code>cd /tmp
curl -O https://wordpress.org/latest.tar.gz
tar -xzvf latest.tar.gz
sudo mv wordpress /var/www/yourdomain.com
</code></pre>



<p class="wp-block-paragraph">Set proper ownership and permissions:</p>



<pre class="wp-block-code"><code>sudo chown -R www-data:www-data /var/www/yourdomain.com
sudo find /var/www/yourdomain.com -type d -exec chmod 755 {} \;
sudo find /var/www/yourdomain.com -type f -exec chmod 644 {} \;
</code></pre>



<p class="wp-block-paragraph">Create the <code>wp-config.php</code> file:</p>



<pre class="wp-block-code"><code>cd /var/www/yourdomain.com
sudo cp wp-config-sample.php wp-config.php
sudo nano wp-config.php
</code></pre>



<p class="wp-block-paragraph">Fill in your database details:</p>



<pre class="wp-block-code"><code>define( 'DB_NAME', 'wordpress_db' );
define( 'DB_USER', 'wp_user' );
define( 'DB_PASSWORD', 'Str0ng-Unique-Password' );
define( 'DB_HOST', 'localhost' );
</code></pre>



<p class="wp-block-paragraph">Also generate fresh authentication keys and salts from the <a href="https://api.wordpress.org/secret-key/1.1/salt/">WordPress secret key generator</a> and paste them into the same file — don&#8217;t leave the placeholder values.</p>



<h2 class="wp-block-heading">Step 5: Create an Apache Virtual Host</h2>



<p class="wp-block-paragraph">Create a new virtual host file:</p>



<pre class="wp-block-code"><code>sudo nano /etc/apache2/sites-available/yourdomain.com.conf
</code></pre>



<pre class="wp-block-code"><code>&lt;VirtualHost *:80&gt;
    ServerName yourdomain.com
    ServerAlias www.yourdomain.com
    DocumentRoot /var/www/yourdomain.com

    &lt;Directory /var/www/yourdomain.com&gt;
        AllowOverride All
        Require all granted
    &lt;/Directory&gt;

    ErrorLog ${APACHE_LOG_DIR}/yourdomain.com-error.log
    CustomLog ${APACHE_LOG_DIR}/yourdomain.com-access.log combined
&lt;/VirtualHost&gt;
</code></pre>



<p class="wp-block-paragraph">The <code>AllowOverride All</code> directive is critical — without it, WordPress&#8217;s <code>.htaccess</code> rules (which control permalinks) will be silently ignored.</p>



<p class="wp-block-paragraph">Enable the site and required modules:</p>



<pre class="wp-block-code"><code>sudo a2ensite yourdomain.com.conf
sudo a2enmod rewrite
sudo a2dissite 000-default.conf
sudo systemctl reload apache2
</code></pre>



<h2 class="wp-block-heading">Step 6: Finish the WordPress Web Installer</h2>



<p class="wp-block-paragraph">Visit <code>http://yourdomain.com</code> in your browser. WordPress&#8217;s famous five-minute installer will walk you through choosing a site title, admin username, and password. Once complete, log in at <code>/wp-admin</code>.</p>



<h2 class="wp-block-heading">Step 7: Enable HTTPS with Let&#8217;s Encrypt</h2>



<p class="wp-block-paragraph">There&#8217;s no good reason to run a WordPress site without HTTPS in 2026. Certbot makes this nearly automatic:</p>



<pre class="wp-block-code"><code>sudo apt install certbot python3-certbot-apache -y
sudo certbot --apache -d yourdomain.com -d www.yourdomain.com
</code></pre>



<p class="wp-block-paragraph">Certbot will edit your virtual host automatically to add a <code>:443</code> block and redirect HTTP to HTTPS. Confirm auto-renewal is scheduled:</p>



<pre class="wp-block-code"><code>sudo certbot renew --dry-run
</code></pre>



<h2 class="wp-block-heading">Real-World Use Cases</h2>



<ul class="wp-block-list">
<li><strong>Small business and portfolio sites</strong> — the classic use case, where simplicity and <code>.htaccess</code>-driven plugin compatibility matter more than raw performance.</li>



<li><strong>Multi-site networks</strong> — Apache&#8217;s virtual host system scales cleanly to dozens of WordPress installs on one box.</li>



<li><strong>Agency hosting</strong> — agencies managing many client sites appreciate how predictable Apache configuration is across environments.</li>



<li><strong>Staging environments</strong> — quick to spin up, easy to tear down, and every WordPress plugin assumes it exists.</li>
</ul>



<h2 class="wp-block-heading">Troubleshooting Common Issues</h2>



<p class="wp-block-paragraph"><strong>White screen of death</strong> Usually a PHP error or memory limit issue. Check:</p>



<pre class="wp-block-code"><code>sudo tail -f /var/log/apache2/yourdomain.com-error.log
</code></pre>



<p class="wp-block-paragraph">Increase PHP memory in <code>wp-config.php</code>:</p>



<pre class="wp-block-code"><code>define( 'WP_MEMORY_LIMIT', '256M' );
</code></pre>



<p class="wp-block-paragraph"><strong>Permalinks return 404</strong> This almost always means <code>mod_rewrite</code> isn&#8217;t enabled or <code>AllowOverride</code> isn&#8217;t set to <code>All</code>.</p>



<pre class="wp-block-code"><code>sudo a2enmod rewrite
sudo systemctl restart apache2
</code></pre>



<p class="wp-block-paragraph">Then go to <strong>Settings &gt; Permalinks</strong> in WP admin and click <strong>Save</strong> to regenerate <code>.htaccess</code>.</p>



<p class="wp-block-paragraph"><strong>&#8220;Error establishing a database connection&#8221;</strong> Check that MySQL is running and the credentials in <code>wp-config.php</code> match exactly:</p>



<pre class="wp-block-code"><code>sudo systemctl status mysql
</code></pre>



<p class="wp-block-paragraph"><strong>File upload/permission errors</strong> Usually ownership drift after manual file edits:</p>



<pre class="wp-block-code"><code>sudo chown -R www-data:www-data /var/www/yourdomain.com
</code></pre>



<h2 class="wp-block-heading">Security Best Practices</h2>



<ul class="wp-block-list">
<li>Keep WordPress core, themes, and plugins updated — most WordPress compromises come from outdated plugins, not Apache itself.</li>



<li>Disable directory listing: <code>&lt;Directory /var/www/yourdomain.com> Options -Indexes&lt;/Directory></code></li>



<li>Block access to <code>wp-config.php</code> explicitly: <code>&lt;Files wp-config.php> Require all denied&lt;/Files></code></li>



<li>Limit login attempts with a plugin or <code>mod_security</code> rule set.</li>



<li>Use strong, unique database credentials — never reuse the root MySQL account.</li>



<li>Set up a Web Application Firewall (<code>mod_security2</code>) for an extra layer of filtering.</li>



<li>Regularly back up both the database and <code>wp-content</code> directory.</li>
</ul>



<h2 class="wp-block-heading">Performance Optimization</h2>



<ul class="wp-block-list">
<li><strong>Enable caching</strong> with a plugin like WP Super Cache or W3 Total Cache, paired with Apache&#8217;s <code>mod_expires</code> and <code>mod_deflate</code>: <code>&lt;IfModule mod_deflate.c> AddOutputFilterByType DEFLATE text/html text/css application/javascript&lt;/IfModule>&lt;IfModule mod_expires.c> ExpiresActive On ExpiresByType image/jpeg "access plus 1 month" ExpiresByType text/css "access plus 1 week"&lt;/IfModule></code></li>



<li><strong>Switch to PHP-FPM</strong> instead of <code>mod_php</code> for better concurrency handling under load.</li>



<li><strong>Enable OPcache</strong> in <code>php.ini</code> to cache compiled PHP bytecode.</li>



<li><strong>Use a CDN</strong> for static assets and images to reduce server load.</li>



<li><strong>Optimize images</strong> before upload — plugins like ShortPixel or Imagify help automate this.</li>



<li><strong>Tune <code>MaxRequestWorkers</code></strong> in Apache&#8217;s MPM config to match your server&#8217;s RAM and expected traffic.</li>
</ul>



<h2 class="wp-block-heading">Frequently Asked Questions</h2>



<p class="wp-block-paragraph"><strong>Do I need <code>mod_rewrite</code> for WordPress?</strong> Yes. Pretty permalinks (<code>/blog/my-post/</code> instead of <code>/?p=123</code>) depend entirely on it.</p>



<p class="wp-block-paragraph"><strong>Can I run multiple WordPress sites on one Apache server?</strong> Yes, using separate virtual host files per domain, each pointing to its own document root and database.</p>



<p class="wp-block-paragraph"><strong>Is Apache slower than Nginx for WordPress?</strong> Under typical traffic, the difference is negligible, especially when paired with PHP-FPM and caching. Nginx has an edge at very high concurrency, but most sites never reach that threshold.</p>



<p class="wp-block-paragraph"><strong>Should I use <code>mod_php</code> or PHP-FPM?</strong> PHP-FPM is generally preferred today for better memory efficiency and concurrent request handling, even when running behind Apache via <code>mod_proxy_fcgi</code>. It also lets you run Apache&#8217;s more efficient event MPM, since <code>mod_php</code> requires the older prefork MPM to work safely.</p>



<p class="wp-block-paragraph"><strong>How do I move a WordPress site from staging to production without breaking links?</strong> Export the database, then run a search-and-replace on the old domain across the SQL dump before importing it on the new server — a plugin like &#8220;Better Search Replace&#8221; handles this safely from within WP admin, since a plain text find-and-replace can corrupt PHP-serialized data stored in some WordPress fields.</p>



<p class="wp-block-paragraph"><strong>Do I need a caching plugin if I already configured mod_expires and mod_deflate?</strong> They solve different problems. Apache&#8217;s <code>mod_expires</code>/<code>mod_deflate</code> control browser-side caching and compression of already-generated pages; a caching plugin (or <code>mod_cache</code>) avoids re-running PHP and database queries on every request by storing a static copy of the rendered page. Using both together gives the best result — fewer server-side computations and smaller, longer-cached responses on the client side.</p>



<h2 class="wp-block-heading">Summary and Key Takeaways</h2>



<p class="wp-block-paragraph">Setting up Apache for WordPress is a well-worn path for a reason — it&#8217;s predictable, flexible, and backed by decades of documentation. The core steps are: install Apache, PHP, and MySQL; configure a virtual host with <code>AllowOverride All</code>; run the WordPress installer; and lock things down with HTTPS and sane file permissions.</p>



<p class="wp-block-paragraph">Once the base install is running, the real work is ongoing maintenance: keeping software updated, watching logs, and tuning caching as traffic grows. Get those fundamentals right and Apache will happily serve a WordPress site for years without drama.</p>



<h2 class="wp-block-heading">References</h2>



<ul class="wp-block-list">
<li><a href="https://httpd.apache.org/docs/">Apache HTTP Server Documentation</a></li>



<li><a href="https://wordpress.org/support/article/how-to-install-wordpress/">WordPress Codex: Installing WordPress</a></li>



<li><a href="https://certbot.eff.org/">Let&#8217;s Encrypt / Certbot Documentation</a></li>



<li><a href="https://httpd.apache.org/docs/current/mod/mod_rewrite.html">Apache mod_rewrite Documentation</a></li>
</ul>



<p class="wp-block-paragraph"></p>
<p>The post <a href="https://awjunaid.com/apache/how-to-set-up-apache-for-serving-wordpress-websites/">How to Set Up Apache for Serving WordPress Websites</a> appeared first on <a href="https://awjunaid.com">Abdul Wahab Junaid</a>.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://awjunaid.com/apache/how-to-set-up-apache-for-serving-wordpress-websites/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">5048</post-id>	</item>
		<item>
		<title>How to Use Apache as a Reverse Proxy for Tomcat</title>
		<link>https://awjunaid.com/apache/how-to-use-apache-as-a-reverse-proxy-for-tomcat/</link>
					<comments>https://awjunaid.com/apache/how-to-use-apache-as-a-reverse-proxy-for-tomcat/?noamp=mobile#respond</comments>
		
		<dc:creator><![CDATA[Abdul Wahab Junaid]]></dc:creator>
		<pubDate>Mon, 11 Sep 2023 11:22:52 +0000</pubDate>
				<category><![CDATA[Apache]]></category>
		<category><![CDATA[apache]]></category>
		<guid isPermaLink="false">https://awjunaid.com/?p=5045</guid>

					<description><![CDATA[<p>Java web applications running on Tomcat are powerful, but Tomcat alone isn&#8217;t really built to be the public-facing&#8230;</p>
<p>The post <a href="https://awjunaid.com/apache/how-to-use-apache-as-a-reverse-proxy-for-tomcat/">How to Use Apache as a Reverse Proxy for Tomcat</a> appeared first on <a href="https://awjunaid.com">Abdul Wahab Junaid</a>.</p>
]]></description>
										<content:encoded><![CDATA[
<p class="wp-block-paragraph">Java web applications running on Tomcat are powerful, but Tomcat alone isn&#8217;t really built to be the public-facing edge of a production website. It doesn&#8217;t handle SSL termination gracefully, it&#8217;s clumsy at serving static assets efficiently, and running it directly on port 80 or 443 means running it as root — never a good idea. The standard fix is to put Apache in front of it as a reverse proxy, letting Apache handle the public traffic while Tomcat quietly does what it&#8217;s good at: running the Java application logic.</p>



<h2 class="wp-block-heading">What Does &#8220;Reverse Proxy&#8221; Actually Mean Here?</h2>



<p class="wp-block-paragraph">A reverse proxy sits between the client and the backend server, forwarding requests to it and returning the backend&#8217;s response to the client as if the proxy had generated it itself. In this setup:</p>



<ol class="wp-block-list">
<li>A browser requests <code>https://yourdomain.com/app</code></li>



<li>Apache receives the request on port 443</li>



<li>Apache forwards it internally to Tomcat on <code>localhost:8080</code></li>



<li>Tomcat processes the request and returns a response</li>



<li>Apache passes that response back to the browser</li>
</ol>



<p class="wp-block-paragraph">The client never talks to Tomcat directly — it only ever sees Apache.</p>



<h2 class="wp-block-heading">Why Put Apache in Front of Tomcat?</h2>



<ul class="wp-block-list">
<li><strong>SSL termination</strong> — Apache&#8217;s <code>mod_ssl</code> handles HTTPS cleanly; Tomcat&#8217;s SSL configuration is more cumbersome by comparison.</li>



<li><strong>Static content offloading</strong> — Apache serves images, CSS, and JS far more efficiently than Tomcat&#8217;s Java-based servlet engine.</li>



<li><strong>Security isolation</strong> — Tomcat never needs to be exposed to the public internet directly; only Apache is.</li>



<li><strong>Load balancing</strong> — Apache can distribute requests across multiple Tomcat instances.</li>



<li><strong>URL flexibility</strong> — you can host multiple applications (some Java, some not) under one domain, routing by path.</li>



<li><strong>Unified logging and access control</strong> — centralize logs, rate limiting, and auth at the Apache layer.</li>
</ul>



<h2 class="wp-block-heading">Prerequisites</h2>



<ul class="wp-block-list">
<li>A Linux server (Ubuntu 22.04/24.04 used in examples)</li>



<li>Root/sudo access</li>



<li>Java installed (OpenJDK 17 or 21 recommended for current Tomcat versions)</li>



<li>Basic familiarity with XML config files (Tomcat&#8217;s <code>server.xml</code>) and Apache&#8217;s config syntax</li>
</ul>



<h2 class="wp-block-heading">Step 1: Install Java and Tomcat</h2>



<pre class="wp-block-code"><code>sudo apt update
sudo apt install openjdk-21-jdk -y
java -version
</code></pre>



<p class="wp-block-paragraph">Download and install Tomcat (adjust the version number as needed):</p>



<pre class="wp-block-code"><code>cd /opt
sudo wget https://dlcdn.apache.org/tomcat/tomcat-10/v10.1.30/bin/apache-tomcat-10.1.30.tar.gz
sudo tar -xzvf apache-tomcat-10.1.30.tar.gz
sudo mv apache-tomcat-10.1.30 tomcat10
</code></pre>



<p class="wp-block-paragraph">Create a dedicated non-root user for Tomcat — never run application servers as root:</p>



<pre class="wp-block-code"><code>sudo useradd -m -U -d /opt/tomcat10 -s /bin/false tomcat
sudo chown -R tomcat:tomcat /opt/tomcat10
sudo chmod +x /opt/tomcat10/bin/*.sh
</code></pre>



<h2 class="wp-block-heading">Step 2: Create a systemd Service for Tomcat</h2>



<pre class="wp-block-code"><code>sudo nano /etc/systemd/system/tomcat.service
</code></pre>



<pre class="wp-block-code"><code>&#91;Unit]
Description=Apache Tomcat
After=network.target

&#91;Service]
Type=forking
User=tomcat
Group=tomcat

Environment="JAVA_HOME=/usr/lib/jvm/java-21-openjdk-amd64"
Environment="CATALINA_PID=/opt/tomcat10/temp/tomcat.pid"
Environment="CATALINA_HOME=/opt/tomcat10"
Environment="CATALINA_BASE=/opt/tomcat10"

ExecStart=/opt/tomcat10/bin/startup.sh
ExecStop=/opt/tomcat10/bin/shutdown.sh

RestartSec=10
Restart=on-failure

&#91;Install]
WantedBy=multi-user.target
</code></pre>



<pre class="wp-block-code"><code>sudo systemctl daemon-reload
sudo systemctl enable tomcat
sudo systemctl start tomcat
sudo systemctl status tomcat
</code></pre>



<p class="wp-block-paragraph">Confirm Tomcat is listening locally:</p>



<pre class="wp-block-code"><code>curl http://localhost:8080
</code></pre>



<p class="wp-block-paragraph">You should see the default Tomcat welcome page HTML returned.</p>



<h2 class="wp-block-heading">Step 3: Install Apache and Required Proxy Modules</h2>



<pre class="wp-block-code"><code>sudo apt install apache2 -y
sudo a2enmod proxy
sudo a2enmod proxy_http
sudo a2enmod proxy_ajp
sudo a2enmod ssl
sudo a2enmod headers
sudo systemctl restart apache2
</code></pre>



<p class="wp-block-paragraph"><code>mod_proxy_http</code> handles standard HTTP proxying; <code>mod_proxy_ajp</code> is an alternative if you prefer the AJP protocol (faster in some setups, but HTTP proxying is simpler and sufficient for most cases).</p>



<h2 class="wp-block-heading">Step 4: Configure the Virtual Host as a Reverse Proxy</h2>



<pre class="wp-block-code"><code>sudo nano /etc/apache2/sites-available/tomcat-proxy.conf
</code></pre>



<pre class="wp-block-code"><code>&lt;VirtualHost *:80&gt;
    ServerName yourdomain.com

    ProxyPreserveHost On
    ProxyRequests Off

    ProxyPass "/" "http://localhost:8080/"
    ProxyPassReverse "/" "http://localhost:8080/"

    ErrorLog ${APACHE_LOG_DIR}/tomcat-proxy-error.log
    CustomLog ${APACHE_LOG_DIR}/tomcat-proxy-access.log combined
&lt;/VirtualHost&gt;
</code></pre>



<p class="wp-block-paragraph">If you only want to proxy a specific application path rather than the whole domain:</p>



<pre class="wp-block-code"><code>ProxyPass "/myapp" "http://localhost:8080/myapp"
ProxyPassReverse "/myapp" "http://localhost:8080/myapp"
</code></pre>



<p class="wp-block-paragraph">Enable the site:</p>



<pre class="wp-block-code"><code>sudo a2ensite tomcat-proxy.conf
sudo a2dissite 000-default.conf
sudo systemctl reload apache2
</code></pre>



<p class="wp-block-paragraph">Visit <code>http://yourdomain.com</code> — you should now see Tomcat&#8217;s response, served transparently through Apache.</p>



<h2 class="wp-block-heading">Step 5: Add HTTPS Termination</h2>



<pre class="wp-block-code"><code>sudo apt install certbot python3-certbot-apache -y
sudo certbot --apache -d yourdomain.com
</code></pre>



<p class="wp-block-paragraph">Certbot adds a <code>&lt;VirtualHost *:443&gt;</code> block automatically. Ensure the proxy directives are duplicated there too, and add headers so Tomcat knows the original request was HTTPS:</p>



<pre class="wp-block-code"><code>&lt;VirtualHost *:443&gt;
    ServerName yourdomain.com

    SSLEngine on
    SSLCertificateFile /etc/letsencrypt/live/yourdomain.com/fullchain.pem
    SSLCertificateKeyFile /etc/letsencrypt/live/yourdomain.com/privkey.pem

    ProxyPreserveHost On
    ProxyPass "/" "http://localhost:8080/"
    ProxyPassReverse "/" "http://localhost:8080/"

    RequestHeader set X-Forwarded-Proto "https"
&lt;/VirtualHost&gt;
</code></pre>



<p class="wp-block-paragraph">Without <code>X-Forwarded-Proto</code>, Tomcat applications that generate absolute URLs may incorrectly generate <code>http://</code> links even when served over HTTPS.</p>



<h2 class="wp-block-heading">Step 6: Configure Tomcat to Trust the Proxy (Optional but Recommended)</h2>



<p class="wp-block-paragraph">In <code>conf/server.xml</code>, add a <code>RemoteIpValve</code> so Tomcat correctly logs the real client IP instead of <code>127.0.0.1</code>:</p>



<pre class="wp-block-code"><code>&lt;Valve className="org.apache.catalina.valves.RemoteIpValve"
       remoteIpHeader="X-Forwarded-For"
       protocolHeader="X-Forwarded-Proto" /&gt;
</code></pre>



<p class="wp-block-paragraph">Restart Tomcat after the change:</p>



<pre class="wp-block-code"><code>sudo systemctl restart tomcat
</code></pre>



<h2 class="wp-block-heading">Real-World Use Cases</h2>



<ul class="wp-block-list">
<li><strong>Legacy Java enterprise apps</strong> — many internal business applications still run on Tomcat/Java EE stacks and need a modern HTTPS-capable front door.</li>



<li><strong>Microservices gateways</strong> — Apache can route different URL paths to different Tomcat instances or even different backend technologies entirely.</li>



<li><strong>Migrating monoliths</strong> — teams gradually moving off a Tomcat monolith often keep Apache as the stable routing layer while backend services change underneath.</li>



<li><strong>Multi-tenant Java hosting</strong> — hosting providers running many customer WAR files on shared Tomcat instances, isolated behind per-domain Apache vhosts.</li>
</ul>



<h2 class="wp-block-heading">Troubleshooting Common Issues</h2>



<p class="wp-block-paragraph"><strong>502 Bad Gateway</strong> Tomcat isn&#8217;t running, or is listening on a different port than configured.</p>



<pre class="wp-block-code"><code>sudo systemctl status tomcat
curl http://localhost:8080
</code></pre>



<p class="wp-block-paragraph"><strong>Proxy works but static assets (CSS/JS) are broken</strong> Usually caused by relative paths inside the Java app assuming it&#8217;s the site root when actually mounted at a subpath. Fix with a <code>ProxyPassReverseCookiePath</code> and confirm the app&#8217;s context path matches your proxy path.</p>



<p class="wp-block-paragraph"><strong>Redirect loops after adding HTTPS</strong> Almost always missing <code>X-Forwarded-Proto</code> — the backend app redirects to <code>http://</code> because it doesn&#8217;t know the original request was HTTPS.</p>



<p class="wp-block-paragraph"><strong>Session stickiness issues with multiple Tomcat instances</strong> If load-balancing across more than one Tomcat node, enable <code>mod_proxy_balancer</code> with session affinity via the <code>route</code> attribute in Tomcat&#8217;s <code>jvmRoute</code>, or move sessions to a shared store (Redis, database).</p>



<h2 class="wp-block-heading">Security Best Practices</h2>



<ul class="wp-block-list">
<li>Bind Tomcat to <code>localhost</code> only (or a private network interface) — never expose port 8080 directly to the internet. <code>&lt;Connector port="8080" address="127.0.0.1" protocol="HTTP/1.1" ... /></code></li>



<li>Remove Tomcat&#8217;s default sample apps (<code>/examples</code>, <code>/docs</code>, <code>/manager</code> if unused) — they&#8217;re common attack surface.</li>



<li>Restrict access to the Tomcat Manager app to specific IPs if it must remain enabled.</li>



<li>Keep both Apache and Tomcat patched — reverse proxy setups are only as secure as the weakest link.</li>



<li>Use <code>mod_security</code> on the Apache layer for an additional WAF pass before requests ever reach Java code.</li>



<li>Set appropriate <code>Header</code> directives to strip or rewrite any internal server info leaking in responses.</li>
</ul>



<h2 class="wp-block-heading">Performance Optimization</h2>



<ul class="wp-block-list">
<li><strong>Enable connection reuse</strong> with <code>mod_proxy</code>&#8216;s keepalive settings: <code>ProxySet keepalive=On</code></li>



<li><strong>Serve static content directly from Apache</strong> instead of proxying it to Tomcat, using <code>Alias</code> for asset directories.</li>



<li><strong>Tune Tomcat&#8217;s thread pool</strong> in <code>server.xml</code> to match expected concurrency: <code>&lt;Connector port="8080" maxThreads="200" minSpareThreads="25" ... /></code></li>



<li><strong>Enable Apache&#8217;s <code>mod_deflate</code></strong> for gzip compression at the edge, reducing bandwidth for JSON/HTML responses from Tomcat.</li>



<li><strong>Use a connection pool</strong> (like Tomcat&#8217;s built-in DBCP) rather than opening new database connections per request.</li>



<li><strong>Monitor with JMX or a tool like VisualVM</strong> to catch memory or thread pool exhaustion before it causes proxy timeouts.</li>
</ul>



<h2 class="wp-block-heading">Frequently Asked Questions</h2>



<p class="wp-block-paragraph"><strong>Should I use <code>mod_proxy_http</code> or <code>mod_proxy_ajp</code>?</strong> HTTP proxying is simpler to configure and debug, and performance differences are minimal for most applications. AJP made more sense historically when HTTP/1.0 overhead was higher; today HTTP proxying is the more common recommendation.</p>



<p class="wp-block-paragraph"><strong>Can Apache load-balance across multiple Tomcat servers?</strong> Yes, using <code>mod_proxy_balancer</code> with a <code>&lt;Proxy balancer://cluster&gt;</code> block listing multiple Tomcat worker nodes, for example:</p>



<pre class="wp-block-code"><code>&lt;Proxy "balancer://tomcatcluster"&gt;
    BalancerMember "http://192.168.1.10:8080" route=node1
    BalancerMember "http://192.168.1.11:8080" route=node2
    ProxySet lbmethod=byrequests
&lt;/Proxy&gt;

ProxyPass "/" "balancer://tomcatcluster/"
ProxyPassReverse "/" "balancer://tomcatcluster/"
</code></pre>



<p class="wp-block-paragraph">Each node&#8217;s <code>route</code> value should match the <code>jvmRoute</code> attribute set in that Tomcat instance&#8217;s <code>server.xml</code>, which lets Apache maintain session affinity so a given user keeps hitting the same backend node for the life of their session.</p>



<p class="wp-block-paragraph"><strong>What happens if a Tomcat node goes down while load balancing?</strong> <code>mod_proxy_balancer</code> marks a failed node as unavailable after a configurable number of failed attempts and routes traffic to the remaining healthy nodes. You can tune this with <code>retry</code> and <code>failonstatus</code> parameters on each <code>BalancerMember</code>, and monitor cluster health live via the built-in balancer-manager handler:</p>



<pre class="wp-block-code"><code>&lt;Location "/balancer-manager"&gt;
    SetHandler balancer-manager
    Require ip 127.0.0.1
&lt;/Location&gt;
</code></pre>



<p class="wp-block-paragraph">This gives a simple web dashboard showing each node&#8217;s status, load, and session count — genuinely useful when debugging uneven traffic distribution.</p>



<p class="wp-block-paragraph"><strong>Do I need to change Tomcat&#8217;s default port?</strong> Not necessarily — since Tomcat is only reachable via <code>localhost</code>, the default 8080 is fine as long as it&#8217;s not exposed externally.</p>



<p class="wp-block-paragraph"><strong>Is this setup suitable for high-traffic production apps?</strong> Yes — this is a standard, widely used pattern in enterprise Java deployments, especially when paired with load balancing and proper JVM tuning.</p>



<h2 class="wp-block-heading">Summary and Key Takeaways</h2>



<p class="wp-block-paragraph">Putting Apache in front of Tomcat gives you the best of both worlds: Apache&#8217;s mature HTTPS handling, static file serving, and access control, combined with Tomcat&#8217;s solid Java servlet execution. The setup boils down to installing both, enabling <code>mod_proxy</code> and <code>mod_proxy_http</code>, writing a <code>ProxyPass</code>/<code>ProxyPassReverse</code> pair, and layering HTTPS with proper forwarded headers on top.</p>



<p class="wp-block-paragraph">Once running, the ongoing focus shifts to security isolation (never expose Tomcat directly), performance tuning (thread pools, keepalive, compression), and keeping both layers patched. This is a proven, boring-in-a-good-way architecture that scales from a single small app to enterprise multi-instance deployments.</p>



<h2 class="wp-block-heading">References</h2>



<ul class="wp-block-list">
<li><a href="https://httpd.apache.org/docs/current/mod/mod_proxy.html">Apache mod_proxy Documentation</a></li>



<li><a href="https://httpd.apache.org/docs/current/mod/mod_proxy_http.html">Apache mod_proxy_http Documentation</a></li>



<li><a href="https://tomcat.apache.org/tomcat-10.1-doc/">Apache Tomcat Documentation</a></li>



<li><a href="https://tomcat.apache.org/tomcat-10.1-doc/config/valve.html#Remote_IP_Valve">Tomcat RemoteIpValve Documentation</a></li>
</ul>



<p class="wp-block-paragraph"></p>
<p>The post <a href="https://awjunaid.com/apache/how-to-use-apache-as-a-reverse-proxy-for-tomcat/">How to Use Apache as a Reverse Proxy for Tomcat</a> appeared first on <a href="https://awjunaid.com">Abdul Wahab Junaid</a>.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://awjunaid.com/apache/how-to-use-apache-as-a-reverse-proxy-for-tomcat/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">5045</post-id>	</item>
		<item>
		<title>How to Serve Static Files Efficiently with Apache</title>
		<link>https://awjunaid.com/apache/how-to-serve-static-files-efficiently-with-apache/</link>
					<comments>https://awjunaid.com/apache/how-to-serve-static-files-efficiently-with-apache/?noamp=mobile#respond</comments>
		
		<dc:creator><![CDATA[Abdul Wahab Junaid]]></dc:creator>
		<pubDate>Mon, 11 Sep 2023 11:21:36 +0000</pubDate>
				<category><![CDATA[Apache]]></category>
		<category><![CDATA[apache]]></category>
		<guid isPermaLink="false">https://awjunaid.com/?p=5042</guid>

					<description><![CDATA[<p>Static files — images, CSS, JavaScript, fonts, downloadable PDFs — usually make up the majority of a website&#8217;s&#8230;</p>
<p>The post <a href="https://awjunaid.com/apache/how-to-serve-static-files-efficiently-with-apache/">How to Serve Static Files Efficiently with Apache</a> appeared first on <a href="https://awjunaid.com">Abdul Wahab Junaid</a>.</p>
]]></description>
										<content:encoded><![CDATA[
<p class="wp-block-paragraph">Static files — images, CSS, JavaScript, fonts, downloadable PDFs — usually make up the majority of a website&#8217;s total requests, even on sites that are technically &#8220;dynamic.&#8221; How efficiently a server handles these files has an outsized effect on page load speed, bandwidth costs, and overall user experience. Apache, despite its reputation as a &#8220;heavier&#8221; server compared to something like Nginx, can serve static content extremely efficiently once it&#8217;s configured properly.</p>



<p class="wp-block-paragraph">This guide covers the specific modules, directives, and tuning steps that make Apache genuinely fast at static file delivery.</p>



<h2 class="wp-block-heading">Why Static File Performance Matters</h2>



<p class="wp-block-paragraph">Every unnecessary byte sent or unnecessary round trip made adds latency. A few compounding effects:</p>



<ul class="wp-block-list">
<li><strong>Page speed directly affects SEO rankings</strong> — Core Web Vitals metrics like Largest Contentful Paint are heavily influenced by static asset delivery time.</li>



<li><strong>Bandwidth costs money</strong> — uncompressed assets and missing cache headers mean repeated, avoidable downloads.</li>



<li><strong>User experience and conversion</strong> — studies consistently show bounce rates climb sharply as load time increases.</li>



<li><strong>Server resource usage</strong> — inefficient static file handling ties up worker processes that could be serving dynamic requests instead.</li>
</ul>



<h2 class="wp-block-heading">Prerequisites</h2>



<ul class="wp-block-list">
<li>A working Apache installation (Ubuntu/Debian examples used below)</li>



<li>Root or sudo access</li>



<li>Basic familiarity with Apache&#8217;s directory and virtual host configuration</li>



<li>An existing site with static assets (images, CSS, JS) to optimize</li>
</ul>



<h2 class="wp-block-heading">Step 1: Enable the Core Performance Modules</h2>



<p class="wp-block-paragraph">A handful of modules do most of the heavy lifting for static content performance:</p>



<pre class="wp-block-code"><code>sudo a2enmod expires
sudo a2enmod headers
sudo a2enmod deflate
sudo a2enmod brotli
sudo a2enmod cache
sudo a2enmod cache_disk
sudo systemctl restart apache2
</code></pre>



<ul class="wp-block-list">
<li><strong><code>mod_expires</code></strong> — sets <code>Expires</code> and <code>Cache-Control</code> headers so browsers cache assets instead of re-requesting them.</li>



<li><strong><code>mod_headers</code></strong> — lets you fine-tune arbitrary response headers.</li>



<li><strong><code>mod_deflate</code></strong> — gzip compression for text-based assets.</li>



<li><strong><code>mod_brotli</code></strong> — Brotli compression, typically 15-20% smaller than gzip for the same content (available in Apache 2.4.26+).</li>



<li><strong><code>mod_cache</code> / <code>mod_cache_disk</code></strong> — server-side caching layer, useful when static files are generated dynamically but rarely change.</li>
</ul>



<h2 class="wp-block-heading">Step 2: Configure Browser Caching with mod_expires</h2>



<p class="wp-block-paragraph">Add this inside your virtual host or in a global config file:</p>



<pre class="wp-block-code"><code>&lt;IfModule mod_expires.c&gt;
    ExpiresActive On

    ExpiresByType image/jpeg "access plus 1 year"
    ExpiresByType image/png "access plus 1 year"
    ExpiresByType image/webp "access plus 1 year"
    ExpiresByType image/svg+xml "access plus 1 year"
    ExpiresByType font/woff2 "access plus 1 year"
    ExpiresByType text/css "access plus 1 month"
    ExpiresByType application/javascript "access plus 1 month"
    ExpiresByType text/html "access plus 1 hour"
&lt;/IfModule&gt;
</code></pre>



<p class="wp-block-paragraph">Long cache lifetimes for images and fonts make sense because they rarely change; shorter lifetimes for HTML avoid serving stale content after a deploy. If you version your CSS/JS filenames (e.g., <code>app.a1b2c3.js</code>), you can safely cache those for a year too, since a new deploy means a new filename.</p>



<h2 class="wp-block-heading">Step 3: Add Cache-Control Headers Explicitly</h2>



<p class="wp-block-paragraph"><code>mod_expires</code> sets <code>Expires</code>, but modern browsers prioritize <code>Cache-Control</code>. Combine both:</p>



<pre class="wp-block-code"><code>&lt;IfModule mod_headers.c&gt;
    &lt;FilesMatch "\.(jpg|jpeg|png|gif|webp|svg|woff2?|ttf|eot)$"&gt;
        Header set Cache-Control "public, max-age=31536000, immutable"
    &lt;/FilesMatch&gt;
    &lt;FilesMatch "\.(css|js)$"&gt;
        Header set Cache-Control "public, max-age=2592000"
    &lt;/FilesMatch&gt;
&lt;/IfModule&gt;
</code></pre>



<p class="wp-block-paragraph">The <code>immutable</code> flag tells supporting browsers not to even revalidate the file before its expiry — a meaningful speedup for repeat visitors.</p>



<h2 class="wp-block-heading">Step 4: Enable Compression</h2>



<pre class="wp-block-code"><code>&lt;IfModule mod_deflate.c&gt;
    AddOutputFilterByType DEFLATE text/html text/plain text/xml text/css
    AddOutputFilterByType DEFLATE application/javascript application/json
    AddOutputFilterByType DEFLATE image/svg+xml
&lt;/IfModule&gt;
</code></pre>



<p class="wp-block-paragraph">If <code>mod_brotli</code> is available, prefer it over gzip for supporting clients — Apache will automatically negotiate based on the client&#8217;s <code>Accept-Encoding</code> header:</p>



<pre class="wp-block-code"><code>&lt;IfModule mod_brotli.c&gt;
    AddOutputFilterByType BROTLI_COMPRESS text/html text/css application/javascript
&lt;/IfModule&gt;
</code></pre>



<p class="wp-block-paragraph">Don&#8217;t bother compressing already-compressed formats like JPEG, PNG, WebP, or MP4 — it wastes CPU for negligible or negative size gains.</p>



<h2 class="wp-block-heading">Step 5: Serve Files with sendfile and Efficient MPM</h2>



<p class="wp-block-paragraph">Apache&#8217;s <code>EnableSendfile</code> directive lets the OS kernel handle file transfer directly, bypassing extra userspace copies:</p>



<pre class="wp-block-code"><code>EnableSendfile On
</code></pre>



<p class="wp-block-paragraph">For static-heavy workloads, the <strong>event MPM</strong> generally outperforms the older prefork MPM because it handles keep-alive connections more efficiently:</p>



<pre class="wp-block-code"><code>sudo a2dismod mpm_prefork
sudo a2enmod mpm_event
sudo systemctl restart apache2
</code></pre>



<p class="wp-block-paragraph">Note: if you&#8217;re running <code>mod_php</code> (not PHP-FPM), you&#8217;ll need to stick with <code>mpm_prefork</code>, since <code>mod_php</code> isn&#8217;t thread-safe. This is another good reason to migrate to PHP-FPM if PHP is in the mix at all.</p>



<h2 class="wp-block-heading">Step 6: Set Up a Dedicated Static Assets Virtual Host (Optional)</h2>



<p class="wp-block-paragraph">For high-traffic sites, separating static content onto its own subdomain or path can simplify caching and CDN integration:</p>



<pre class="wp-block-code"><code>&lt;VirtualHost *:80&gt;
    ServerName static.yourdomain.com
    DocumentRoot /var/www/static

    &lt;Directory /var/www/static&gt;
        Options -Indexes +FollowSymLinks
        AllowOverride None
        Require all granted
    &lt;/Directory&gt;

    &lt;IfModule mod_headers.c&gt;
        Header set Cache-Control "public, max-age=31536000, immutable"
        Header unset ETag
    &lt;/IfModule&gt;
    FileETag None
&lt;/VirtualHost&gt;
</code></pre>



<p class="wp-block-paragraph">Disabling <code>ETag</code> here is intentional — when serving from multiple servers or after a restart, inode-based ETags can mismatch and cause unnecessary revalidation; a long <code>Cache-Control</code> max-age already covers the caching need.</p>



<h2 class="wp-block-heading">Step 7: Offload to a CDN (Recommended for Production)</h2>



<p class="wp-block-paragraph">Even a perfectly tuned Apache server benefits from a CDN in front of it for static assets — reduced latency via edge locations, reduced origin load, and built-in DDoS mitigation. Popular options include Cloudflare, AWS CloudFront, and Bunny CDN. Point the CDN at your static virtual host or asset path, and let it handle edge caching using the <code>Cache-Control</code> headers you&#8217;ve already configured.</p>



<h2 class="wp-block-heading">Real-World Use Cases</h2>



<ul class="wp-block-list">
<li><strong>E-commerce product images</strong> — thousands of product photos benefit enormously from long-lived caching and compression.</li>



<li><strong>SaaS dashboards</strong> — CSS/JS bundles served with immutable caching dramatically speed up repeat logins.</li>



<li><strong>Documentation sites</strong> — often almost entirely static, making this exact tuning the single biggest performance lever available.</li>



<li><strong>Media/download sites</strong> — large file downloads benefit from <code>sendfile</code> and proper <code>Content-Disposition</code> headers.</li>
</ul>



<h2 class="wp-block-heading">Troubleshooting Common Issues</h2>



<p class="wp-block-paragraph"><strong>Assets aren&#8217;t being cached despite the config</strong> Check the actual response headers:</p>



<pre class="wp-block-code"><code>curl -I https://yourdomain.com/style.css
</code></pre>



<p class="wp-block-paragraph">Look for <code>Cache-Control</code> and <code>Expires</code> in the output. If missing, confirm the modules are enabled:</p>



<pre class="wp-block-code"><code>apache2ctl -M | grep -E "expires|headers|deflate"
</code></pre>



<p class="wp-block-paragraph"><strong>Compression isn&#8217;t happening</strong> Verify with:</p>



<pre class="wp-block-code"><code>curl -H "Accept-Encoding: gzip" -I https://yourdomain.com/script.js
</code></pre>



<p class="wp-block-paragraph">Look for <code>Content-Encoding: gzip</code> in the response. If absent, check that the MIME type in your <code>AddOutputFilterByType</code> matches what&#8217;s actually being served — a mismatched <code>Content-Type</code> header will silently skip compression.</p>



<p class="wp-block-paragraph"><strong>Old cached versions serving after deploy</strong> This is a caching success, not a failure — but it means you need cache-busting. Use versioned filenames or query strings (<code>style.css?v=2</code>) so browsers fetch the new file.</p>



<p class="wp-block-paragraph"><strong>High CPU from mod_deflate</strong> Compression has a CPU cost. If your server is CPU-bound rather than bandwidth-bound, consider lowering the compression level:</p>



<pre class="wp-block-code"><code>DeflateCompressionLevel 6
</code></pre>



<p class="wp-block-paragraph">(Default is 9; 6 is a common speed/ratio compromise.)</p>



<h2 class="wp-block-heading">Security Best Practices</h2>



<ul class="wp-block-list">
<li>Disable directory listing everywhere static files are served: <code>Options -Indexes</code></li>



<li>Prevent execution of scripts in upload/static directories: <code>&lt;Directory /var/www/static/uploads> php_admin_flag engine off RemoveHandler .php .phtml&lt;/Directory></code></li>



<li>Set <code>X-Content-Type-Options: nosniff</code> to prevent MIME-sniffing attacks: <code>Header set X-Content-Type-Options "nosniff"</code></li>



<li>Restrict access to sensitive static files (backups, config files, <code>.git</code> directories): <code>&lt;FilesMatch "^\.(git|env|htaccess)"> Require all denied&lt;/FilesMatch></code></li>



<li>Keep Apache and its modules patched — static file serving is a common target for path traversal attempts.</li>
</ul>



<h2 class="wp-block-heading">Performance Optimization Checklist</h2>



<ul class="wp-block-list">
<li>Enable <code>mod_expires</code> and <code>mod_headers</code> for long-lived cache headers</li>



<li>Enable <code>mod_deflate</code> and/or <code>mod_brotli</code> for text-based assets</li>



<li>Use <code>EnableSendfile On</code> and the event MPM where possible</li>



<li>Version or hash static filenames for safe long-term caching</li>



<li>Serve images in modern formats (WebP/AVIF) alongside fallbacks</li>



<li>Put a CDN in front of static assets for production sites</li>



<li>Disable ETags in multi-server setups to avoid revalidation mismatches</li>



<li>Regularly audit uncompressed or uncached assets with browser dev tools or Lighthouse</li>
</ul>



<h2 class="wp-block-heading">Frequently Asked Questions</h2>



<p class="wp-block-paragraph"><strong>Does Apache&#8217;s static file performance really compete with Nginx?</strong> For most real-world traffic levels, yes — especially with <code>mod_event</code>, sendfile, and proper caching enabled. Nginx&#8217;s edge tends to show up mainly under extremely high concurrency (tens of thousands of simultaneous connections).</p>



<p class="wp-block-paragraph"><strong>Should I compress images with mod_deflate?</strong> No. JPEG, PNG, and WebP are already compressed formats; running them through gzip/Brotli wastes CPU for little to no size reduction. Compression should target text-based assets.</p>



<p class="wp-block-paragraph"><strong>What&#8217;s the difference between Expires and Cache-Control?</strong> <code>Expires</code> is the older HTTP/1.0-era header specifying an absolute date; <code>Cache-Control</code> (HTTP/1.1) is more flexible, using relative <code>max-age</code> values and additional directives like <code>immutable</code> and <code>no-cache</code>. Modern browsers prioritize <code>Cache-Control</code> when both are present.</p>



<p class="wp-block-paragraph"><strong>Is a CDN necessary if Apache is already tuned well?</strong> Not strictly necessary, but highly recommended for production sites with geographically distributed visitors — a CDN reduces latency in ways origin-server tuning alone can&#8217;t.</p>



<h2 class="wp-block-heading">Summary and Key Takeaways</h2>



<p class="wp-block-paragraph">Serving static files efficiently with Apache comes down to a short list of well-understood levers: enable long-lived browser caching with <code>mod_expires</code> and <code>mod_headers</code>, compress text assets with <code>mod_deflate</code>/<code>mod_brotli</code>, let the kernel handle file transfer with <code>sendfile</code>, and choose the event MPM when your stack allows it. Layer a CDN on top for production traffic, and you&#8217;ve got a static file delivery setup that competes with anything else on the market.</p>



<p class="wp-block-paragraph">None of this requires exotic configuration — it&#8217;s a handful of directives applied consistently, then verified with <code>curl -I</code> and real browser testing. Get these fundamentals in place once, and static asset delivery stops being something you have to think about.</p>



<h2 class="wp-block-heading">References</h2>



<ul class="wp-block-list">
<li><a href="https://httpd.apache.org/docs/current/mod/mod_expires.html">Apache mod_expires Documentation</a></li>



<li><a href="https://httpd.apache.org/docs/current/mod/mod_deflate.html">Apache mod_deflate Documentation</a></li>



<li><a href="https://httpd.apache.org/docs/current/mod/mod_headers.html">Apache mod_headers Documentation</a></li>



<li><a href="https://httpd.apache.org/docs/current/misc/perf-tuning.html">Apache Performance Tuning Guide</a></li>



<li><a href="https://httpd.apache.org/docs/current/mpm.html">Apache MPM Documentation</a></li>
</ul>
<p>The post <a href="https://awjunaid.com/apache/how-to-serve-static-files-efficiently-with-apache/">How to Serve Static Files Efficiently with Apache</a> appeared first on <a href="https://awjunaid.com">Abdul Wahab Junaid</a>.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://awjunaid.com/apache/how-to-serve-static-files-efficiently-with-apache/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">5042</post-id>	</item>
		<item>
		<title>How to Configure Apache for Content Compression</title>
		<link>https://awjunaid.com/apache/how-to-configure-apache-for-content-compression/</link>
					<comments>https://awjunaid.com/apache/how-to-configure-apache-for-content-compression/?noamp=mobile#respond</comments>
		
		<dc:creator><![CDATA[Abdul Wahab Junaid]]></dc:creator>
		<pubDate>Mon, 11 Sep 2023 11:20:11 +0000</pubDate>
				<category><![CDATA[Apache]]></category>
		<category><![CDATA[apache]]></category>
		<guid isPermaLink="false">https://awjunaid.com/?p=5039</guid>

					<description><![CDATA[<p>If a website feels sluggish, one of the first things I check is whether compression is turned on.&#8230;</p>
<p>The post <a href="https://awjunaid.com/apache/how-to-configure-apache-for-content-compression/">How to Configure Apache for Content Compression</a> appeared first on <a href="https://awjunaid.com">Abdul Wahab Junaid</a>.</p>
]]></description>
										<content:encoded><![CDATA[
<p class="wp-block-paragraph">If a website feels sluggish, one of the first things I check is whether compression is turned on. It&#8217;s one of those settings that takes minutes to configure but can shave a huge chunk off page load times. In this guide, I&#8217;ll walk through exactly how to set up content compression on Apache, why it matters, and how to avoid the common mistakes that trip people up.</p>



<h2 class="wp-block-heading">What Is Content Compression, and Why Does It Matter?</h2>



<p class="wp-block-paragraph">Content compression is the process of shrinking the size of files (HTML, CSS, JavaScript, JSON, XML, and more) before Apache sends them to a visitor&#8217;s browser. The browser then decompresses the file on arrival and renders it normally. The visitor never notices anything except that the page loaded faster.</p>



<p class="wp-block-paragraph">Apache handles this through two modules:</p>



<ul class="wp-block-list">
<li><strong>mod_deflate</strong> – uses the Gzip compression algorithm, widely supported and the de facto standard for years.</li>



<li><strong>mod_brotli</strong> – uses Google&#8217;s Brotli algorithm, which typically compresses better than Gzip but requires a newer Apache build and isn&#8217;t available everywhere.</li>
</ul>



<p class="wp-block-paragraph">Compression matters because:</p>



<ul class="wp-block-list">
<li><strong>Faster load times</strong> – smaller files transfer faster, especially on mobile or slow connections.</li>



<li><strong>Lower bandwidth costs</strong> – less data transferred means lower hosting bills, especially at scale.</li>



<li><strong>Better SEO</strong> – page speed is a ranking factor for search engines, and compression is one of the easiest wins.</li>



<li><strong>Improved user experience</strong> – faster sites reduce bounce rates and improve conversions.</li>
</ul>



<h2 class="wp-block-heading">Prerequisites</h2>



<p class="wp-block-paragraph">Before diving in, make sure you have:</p>



<ul class="wp-block-list">
<li>A server running Apache 2.4+ (most modern Linux distributions ship with this)</li>



<li>Root or sudo access to the server</li>



<li>Basic familiarity with the command line and editing configuration files</li>



<li>Apache installed via a package manager (apt, yum/dnf) or compiled from source</li>
</ul>



<h2 class="wp-block-heading">Enabling mod_deflate</h2>



<h3 class="wp-block-heading">Step 1: Check if the Module Is Installed</h3>



<p class="wp-block-paragraph">On most distributions, <code>mod_deflate</code> ships with Apache by default. Verify it&#8217;s available:</p>



<pre class="wp-block-code"><code>apache2ctl -M | grep deflate
</code></pre>



<p class="wp-block-paragraph">On CentOS/RHEL systems, use:</p>



<pre class="wp-block-code"><code>httpd -M | grep deflate
</code></pre>



<p class="wp-block-paragraph">If you see <code>deflate_module (shared)</code> in the output, it&#8217;s already loaded. If not, enable it.</p>



<h3 class="wp-block-heading">Step 2: Enable the Module</h3>



<p class="wp-block-paragraph"><strong>On Debian/Ubuntu:</strong></p>



<pre class="wp-block-code"><code>sudo a2enmod deflate
sudo systemctl restart apache2
</code></pre>



<p class="wp-block-paragraph"><strong>On CentOS/RHEL:</strong></p>



<p class="wp-block-paragraph">The module is usually compiled in by default. If it&#8217;s missing from your <code>httpd.conf</code>, add this line:</p>



<pre class="wp-block-code"><code>LoadModule deflate_module modules/mod_deflate.so
</code></pre>



<p class="wp-block-paragraph">Then restart:</p>



<pre class="wp-block-code"><code>sudo systemctl restart httpd
</code></pre>



<h3 class="wp-block-heading">Step 3: Configure Compression Rules</h3>



<p class="wp-block-paragraph">Open your Apache configuration file (commonly <code>/etc/apache2/apache2.conf</code>, <code>/etc/apache2/conf-available/deflate.conf</code>, or a virtual host file) and add the following block:</p>



<pre class="wp-block-code"><code>&lt;IfModule mod_deflate.c&gt;
    # Compress HTML, CSS, JavaScript, Text, XML and fonts
    AddOutputFilterByType DEFLATE application/javascript
    AddOutputFilterByType DEFLATE application/rss+xml
    AddOutputFilterByType DEFLATE application/vnd.ms-fontobject
    AddOutputFilterByType DEFLATE application/x-font
    AddOutputFilterByType DEFLATE application/x-font-opentype
    AddOutputFilterByType DEFLATE application/x-font-otf
    AddOutputFilterByType DEFLATE application/x-font-truetype
    AddOutputFilterByType DEFLATE application/x-font-ttf
    AddOutputFilterByType DEFLATE application/x-javascript
    AddOutputFilterByType DEFLATE application/xhtml+xml
    AddOutputFilterByType DEFLATE application/xml
    AddOutputFilterByType DEFLATE font/opentype
    AddOutputFilterByType DEFLATE font/otf
    AddOutputFilterByType DEFLATE font/ttf
    AddOutputFilterByType DEFLATE image/svg+xml
    AddOutputFilterByType DEFLATE image/x-icon
    AddOutputFilterByType DEFLATE text/css
    AddOutputFilterByType DEFLATE text/html
    AddOutputFilterByType DEFLATE text/javascript
    AddOutputFilterByType DEFLATE text/plain
    AddOutputFilterByType DEFLATE text/xml

    # Don't compress already-compressed formats
    SetEnvIfNoCase Request_URI \.(?:gif|jpe?g|png|rar|zip|exe|flv|mov|wma|mp3|avi|swf|mp?g|mp4|webm|webp)$ no-gzip

    # Remove the 'Vary' header for proxies that don't handle it correctly
    &lt;IfModule mod_headers.c&gt;
        Header append Vary User-Agent env=!dont-vary
    &lt;/IfModule&gt;
&lt;/IfModule&gt;
</code></pre>



<p class="wp-block-paragraph">Save the file, test the configuration, and restart Apache:</p>



<pre class="wp-block-code"><code>sudo apachectl configtest
sudo systemctl restart apache2
</code></pre>



<h2 class="wp-block-heading">Setting the Compression Level (Optional Tuning)</h2>



<p class="wp-block-paragraph">You can control how aggressively Apache compresses content using <code>DeflateCompressionLevel</code>, which ranges from 1 (fastest, least compression) to 9 (slowest, best compression):</p>



<pre class="wp-block-code"><code>DeflateCompressionLevel 6
</code></pre>



<p class="wp-block-paragraph">Level 6 is a good balance between CPU usage and compression ratio for most sites. Going to 9 rarely provides meaningful savings but noticeably increases CPU load under heavy traffic.</p>



<h2 class="wp-block-heading">Enabling Brotli for Even Better Compression</h2>



<p class="wp-block-paragraph">If your Apache build supports Brotli (2.4.26+), it generally compresses 15-25% better than Gzip for text-based assets.</p>



<p class="wp-block-paragraph"><strong>Install the module (Debian/Ubuntu):</strong></p>



<pre class="wp-block-code"><code>sudo apt install brotli
sudo a2enmod brotli
sudo systemctl restart apache2
</code></pre>



<p class="wp-block-paragraph"><strong>Configuration:</strong></p>



<pre class="wp-block-code"><code>&lt;IfModule mod_brotli.c&gt;
    AddOutputFilterByType BROTLI_COMPRESS text/html text/plain text/css application/javascript application/json
&lt;/IfModule&gt;
</code></pre>



<p class="wp-block-paragraph">Many setups configure Apache to serve Brotli to browsers that support it and fall back to Gzip for older browsers, which Apache negotiates automatically based on the <code>Accept-Encoding</code> header sent by the client.</p>



<h2 class="wp-block-heading">Verifying Compression Is Working</h2>



<p class="wp-block-paragraph">Once configured, confirm compression is actually happening:</p>



<p class="wp-block-paragraph"><strong>Using curl:</strong></p>



<pre class="wp-block-code"><code>curl -H "Accept-Encoding: gzip" -I https://yourdomain.com
</code></pre>



<p class="wp-block-paragraph">Look for <code>Content-Encoding: gzip</code> in the response headers.</p>



<p class="wp-block-paragraph"><strong>Using browser DevTools:</strong></p>



<p class="wp-block-paragraph">Open Network tab → click a request → check the Response Headers for <code>Content-Encoding: gzip</code> or <code>br</code>.</p>



<p class="wp-block-paragraph"><strong>Online tools:</strong></p>



<p class="wp-block-paragraph">Sites like GTmetrix or the &#8220;Check GZIP Compression&#8221; tools will confirm compression and show the percentage of savings.</p>



<h2 class="wp-block-heading">Real-World Use Cases</h2>



<ul class="wp-block-list">
<li><strong>E-commerce sites</strong> with large product catalogs and heavy CSS/JS bundles benefit enormously — compression can cut page weight by 60-80% on text assets.</li>



<li><strong>News and content sites</strong> serving large amounts of HTML text see faster time-to-first-byte perception.</li>



<li><strong>API servers</strong> returning JSON payloads can compress responses significantly, which matters for mobile clients on constrained networks.</li>



<li><strong>Single Page Applications (SPAs)</strong> with large JavaScript bundles see some of the biggest gains, since JS often compresses very well.</li>
</ul>



<h2 class="wp-block-heading">Common Mistakes to Avoid</h2>



<ol class="wp-block-list">
<li><strong>Compressing already-compressed files</strong> – Images (JPEG, PNG), videos, and ZIP files are already compressed. Trying to Gzip them wastes CPU and can even increase file size slightly.</li>



<li><strong>Forgetting to restart Apache</strong> – Configuration changes don&#8217;t take effect until Apache is reloaded or restarted.</li>



<li><strong>Setting compression level too high</strong> – Level 9 barely improves ratio over level 6 but costs significantly more CPU under load.</li>



<li><strong>Not testing with real headers</strong> – Some CDNs or proxies strip the <code>Accept-Encoding</code> header, which prevents compression from happening even if the server is configured correctly.</li>



<li><strong>Ignoring the Vary header</strong> – Missing this header can cause caching proxies to serve compressed content to clients that don&#8217;t support it, breaking the page.</li>
</ol>



<h2 class="wp-block-heading">Security Considerations</h2>



<p class="wp-block-paragraph">Compression isn&#8217;t purely a performance feature — it has a security dimension too.</p>



<ul class="wp-block-list">
<li><strong>BREACH attack</strong> – Compression can, in specific scenarios, leak information about secrets embedded in HTTP responses (like CSRF tokens) when combined with attacker-controlled input reflected in the same response. Mitigate this by disabling compression on pages that reflect user input alongside secrets, or by using CSRF tokens that aren&#8217;t length-predictable.</li>



<li><strong>Rate-limit compression on dynamic content</strong> if you&#8217;re concerned about CPU exhaustion from malicious high-volume requests targeting compressible endpoints.</li>
</ul>



<h2 class="wp-block-heading">Performance Optimization Tips</h2>



<ul class="wp-block-list">
<li>Combine compression with <strong>browser caching</strong> (see our companion guide on Apache caching) for maximum effect.</li>



<li>Pre-compress static assets at build time (e.g., generate <code>.gz</code> versions of CSS/JS during deployment) and serve them directly with <code>mod_deflate</code>&#8216;s static file support, reducing CPU overhead on every request.</li>



<li>Monitor CPU usage after enabling compression, especially on high-traffic servers with limited cores.</li>



<li>Use a CDN in front of Apache; most CDNs handle compression at the edge, reducing load on your origin server.</li>
</ul>



<h2 class="wp-block-heading">Troubleshooting</h2>



<p class="wp-block-paragraph"><strong>Compression not applying:</strong></p>



<ul class="wp-block-list">
<li>Confirm the module is enabled with <code>apache2ctl -M</code>.</li>



<li>Check that your MIME types match what&#8217;s actually being served (<code>text/html</code> vs <code>text/html; charset=UTF-8</code> can behave differently on older Apache versions).</li>



<li>Clear any caching layer (CDN, browser cache) that might be serving stale, uncompressed responses.</li>
</ul>



<p class="wp-block-paragraph"><strong>High CPU usage after enabling:</strong></p>



<ul class="wp-block-list">
<li>Lower the <code>DeflateCompressionLevel</code>.</li>



<li>Consider pre-compressing static files instead of compressing on every request.</li>
</ul>



<p class="wp-block-paragraph"><strong>Errors after config changes:</strong></p>



<ul class="wp-block-list">
<li>Always run <code>apachectl configtest</code> before restarting to catch syntax errors early.</li>
</ul>



<h2 class="wp-block-heading">Frequently Asked Questions</h2>



<p class="wp-block-paragraph"><strong>Does compression affect image files?</strong> No, and it shouldn&#8217;t. Images should be excluded from compression rules since formats like JPEG and PNG are already compressed.</p>



<p class="wp-block-paragraph"><strong>Is Brotli always better than Gzip?</strong> Generally yes for compression ratio, but Gzip has broader compatibility and slightly faster compression speed in some cases. Serving both and letting the browser choose is the safest approach.</p>



<p class="wp-block-paragraph"><strong>Will compression break my site?</strong> Properly configured, no. Issues typically arise from double-compression (e.g., compressing at both the CDN and origin) or missing Vary headers.</p>



<p class="wp-block-paragraph"><strong>How much can I expect page size to shrink?</strong> Text-based assets (HTML, CSS, JS) typically compress by 60-80%. Overall page weight reduction depends on how much of your page is text versus already-compressed media.</p>



<h2 class="wp-block-heading">Summary and Key Takeaways</h2>



<ul class="wp-block-list">
<li>Content compression reduces file sizes before they&#8217;re sent to browsers, cutting load times and bandwidth costs.</li>



<li><code>mod_deflate</code> (Gzip) is the standard choice; <code>mod_brotli</code> offers better ratios where supported.</li>



<li>Configure compression by MIME type, exclude already-compressed formats, and set a sensible compression level (6 is a solid default).</li>



<li>Always test with <code>apachectl configtest</code> before restarting, and verify with curl or browser DevTools.</li>



<li>Be aware of the BREACH attack vector when compressing dynamic pages with reflected secrets.</li>



<li>Pair compression with caching and pre-compression at build time for the best performance gains.</li>
</ul>



<h2 class="wp-block-heading">References</h2>



<ul class="wp-block-list">
<li><a href="https://httpd.apache.org/docs/2.4/mod/mod_deflate.html">Apache mod_deflate Documentation</a></li>



<li><a href="https://httpd.apache.org/docs/2.4/mod/mod_brotli.html">Apache mod_brotli Documentation</a></li>



<li><a href="https://httpd.apache.org/docs/2.4/mod/mod_headers.html">Apache mod_headers Documentation</a></li>
</ul>



<p class="wp-block-paragraph"></p>
<p>The post <a href="https://awjunaid.com/apache/how-to-configure-apache-for-content-compression/">How to Configure Apache for Content Compression</a> appeared first on <a href="https://awjunaid.com">Abdul Wahab Junaid</a>.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://awjunaid.com/apache/how-to-configure-apache-for-content-compression/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">5039</post-id>	</item>
		<item>
		<title>How to Set Up Apache for Content Caching</title>
		<link>https://awjunaid.com/apache/how-to-set-up-apache-for-content-caching/</link>
					<comments>https://awjunaid.com/apache/how-to-set-up-apache-for-content-caching/?noamp=mobile#respond</comments>
		
		<dc:creator><![CDATA[Abdul Wahab Junaid]]></dc:creator>
		<pubDate>Mon, 11 Sep 2023 11:18:41 +0000</pubDate>
				<category><![CDATA[Apache]]></category>
		<category><![CDATA[apache]]></category>
		<guid isPermaLink="false">https://awjunaid.com/?p=5036</guid>

					<description><![CDATA[<p>Every time I audit a slow website, caching is near the top of my checklist. It&#8217;s one of&#8230;</p>
<p>The post <a href="https://awjunaid.com/apache/how-to-set-up-apache-for-content-caching/">How to Set Up Apache for Content Caching</a> appeared first on <a href="https://awjunaid.com">Abdul Wahab Junaid</a>.</p>
]]></description>
										<content:encoded><![CDATA[
<p class="wp-block-paragraph">Every time I audit a slow website, caching is near the top of my checklist. It&#8217;s one of those unglamorous settings that quietly does a lot of heavy lifting, cutting server load and making repeat visits feel almost instant. In this guide, I&#8217;ll explain how Apache caching works and walk through a full setup, from browser caching headers to server-side caching with mod_cache.</p>



<h2 class="wp-block-heading">What Is Content Caching, and Why Does It Matter?</h2>



<p class="wp-block-paragraph">Caching stores a copy of content so it can be reused for future requests instead of being regenerated or re-fetched from scratch every time. In Apache, there are two main layers of caching to think about:</p>



<ol class="wp-block-list">
<li><strong>Client-side (browser) caching</strong> – tells the visitor&#8217;s browser to store static assets (images, CSS, JS) locally so it doesn&#8217;t need to re-download them on every visit.</li>



<li><strong>Server-side caching</strong> – Apache itself stores generated content (like the output of a dynamic PHP page) in memory or on disk, serving the cached version to subsequent visitors instead of regenerating it each time.</li>
</ol>



<p class="wp-block-paragraph">Why it matters:</p>



<ul class="wp-block-list">
<li><strong>Reduced server load</strong> – fewer requests hit your application or database layer.</li>



<li><strong>Faster page loads</strong> – cached assets load from local browser storage or Apache&#8217;s cache instantly.</li>



<li><strong>Lower bandwidth usage</strong> – repeat visitors don&#8217;t re-download unchanged files.</li>



<li><strong>Better scalability</strong> – your server can handle more concurrent visitors with the same hardware.</li>
</ul>



<h2 class="wp-block-heading">Prerequisites</h2>



<ul class="wp-block-list">
<li>Apache 2.4+ installed with root/sudo access</li>



<li><code>mod_expires</code> and <code>mod_headers</code> available (both ship with Apache by default)</li>



<li><code>mod_cache</code>, <code>mod_cache_disk</code>, and <code>mod_cache_socache</code> for server-side caching</li>



<li>Basic understanding of HTTP headers (Cache-Control, Expires, ETag)</li>
</ul>



<h2 class="wp-block-heading">Part 1: Browser Caching with mod_expires and mod_headers</h2>



<h3 class="wp-block-heading">Step 1: Enable the Modules</h3>



<p class="wp-block-paragraph"><strong>Debian/Ubuntu:</strong></p>



<pre class="wp-block-code"><code>sudo a2enmod expires
sudo a2enmod headers
sudo systemctl restart apache2
</code></pre>



<p class="wp-block-paragraph"><strong>CentOS/RHEL:</strong></p>



<p class="wp-block-paragraph">Ensure these lines exist in <code>httpd.conf</code>:</p>



<pre class="wp-block-code"><code>LoadModule expires_module modules/mod_expires.so
LoadModule headers_module modules/mod_headers.so
</code></pre>



<p class="wp-block-paragraph">Then restart:</p>



<pre class="wp-block-code"><code>sudo systemctl restart httpd
</code></pre>



<h3 class="wp-block-heading">Step 2: Configure Expiration Rules</h3>



<p class="wp-block-paragraph">Add this block to your virtual host file or <code>.htaccess</code>:</p>



<pre class="wp-block-code"><code>&lt;IfModule mod_expires.c&gt;
    ExpiresActive On

    # Images
    ExpiresByType image/jpg "access plus 1 year"
    ExpiresByType image/jpeg "access plus 1 year"
    ExpiresByType image/gif "access plus 1 year"
    ExpiresByType image/png "access plus 1 year"
    ExpiresByType image/webp "access plus 1 year"
    ExpiresByType image/svg+xml "access plus 1 year"
    ExpiresByType image/x-icon "access plus 1 year"

    # Video
    ExpiresByType video/mp4 "access plus 1 year"

    # CSS, JavaScript
    ExpiresByType text/css "access plus 1 month"
    ExpiresByType application/javascript "access plus 1 month"
    ExpiresByType text/javascript "access plus 1 month"

    # Fonts
    ExpiresByType font/woff2 "access plus 1 year"
    ExpiresByType font/woff "access plus 1 year"
    ExpiresByType application/vnd.ms-fontobject "access plus 1 year"

    # HTML - short cache since content changes often
    ExpiresByType text/html "access plus 0 seconds"

    # Default
    ExpiresDefault "access plus 2 days"
&lt;/IfModule&gt;
</code></pre>



<h3 class="wp-block-heading">Step 3: Add Cache-Control Headers</h3>



<p class="wp-block-paragraph"><code>Expires</code> headers are somewhat legacy; modern setups pair them with <code>Cache-Control</code> for finer-grained control:</p>



<pre class="wp-block-code"><code>&lt;IfModule mod_headers.c&gt;
    &lt;FilesMatch "\.(ico|jpg|jpeg|png|gif|webp|svg|css|js|woff|woff2)$"&gt;
        Header set Cache-Control "max-age=31536000, public, immutable"
    &lt;/FilesMatch&gt;

    &lt;FilesMatch "\.(html|htm)$"&gt;
        Header set Cache-Control "max-age=0, no-cache, must-revalidate"
    &lt;/FilesMatch&gt;
&lt;/IfModule&gt;
</code></pre>



<p class="wp-block-paragraph">The <code>immutable</code> directive tells modern browsers not to even revalidate the file until it expires — useful for versioned static assets (e.g., <code>style.abc123.css</code>).</p>



<h2 class="wp-block-heading">Part 2: Server-Side Caching with mod_cache</h2>



<p class="wp-block-paragraph">Server-side caching is especially useful for dynamic content (PHP, proxied backends) that&#8217;s expensive to regenerate on every request.</p>



<h3 class="wp-block-heading">Step 1: Enable Required Modules</h3>



<pre class="wp-block-code"><code>sudo a2enmod cache
sudo a2enmod cache_disk
sudo systemctl restart apache2
</code></pre>



<h3 class="wp-block-heading">Step 2: Configure Disk Caching</h3>



<p class="wp-block-paragraph">Add this to your Apache config:</p>



<pre class="wp-block-code"><code>&lt;IfModule mod_cache.c&gt;
    &lt;IfModule mod_cache_disk.c&gt;
        CacheRoot /var/cache/apache2/mod_cache_disk
        CacheEnable disk /
        CacheDirLevels 2
        CacheDirLength 1
        CacheMaxFileSize 5000000
        CacheMinFileSize 1
        CacheIgnoreHeaders Set-Cookie
        CacheDefaultExpire 3600
        CacheMaxExpire 86400
        CacheLastModifiedFactor 0.5
    &lt;/IfModule&gt;
&lt;/IfModule&gt;
</code></pre>



<p class="wp-block-paragraph">Create the cache directory and set proper permissions:</p>



<pre class="wp-block-code"><code>sudo mkdir -p /var/cache/apache2/mod_cache_disk
sudo chown -R www-data:www-data /var/cache/apache2/mod_cache_disk
</code></pre>



<p class="wp-block-paragraph">On CentOS/RHEL, the Apache user is typically <code>apache</code> instead of <code>www-data</code>.</p>



<h3 class="wp-block-heading">Step 3: Restart and Test</h3>



<pre class="wp-block-code"><code>sudo apachectl configtest
sudo systemctl restart apache2
</code></pre>



<p class="wp-block-paragraph">Verify caching is active by checking response headers:</p>



<pre class="wp-block-code"><code>curl -I https://yourdomain.com
</code></pre>



<p class="wp-block-paragraph">Look for an <code>X-Cache</code> or <code>Age</code> header, depending on your configuration, indicating whether the response came from cache.</p>



<h2 class="wp-block-heading">Using mod_cache_socache for Shared Memory Caching</h2>



<p class="wp-block-paragraph">For high-traffic sites, an in-memory cache is faster than disk. <code>mod_cache_socache</code> works with a shared object cache provider like <code>mod_socache_shmcb</code>:</p>



<pre class="wp-block-code"><code>sudo a2enmod cache_socache
sudo a2enmod socache_shmcb
</code></pre>



<pre class="wp-block-code"><code>&lt;IfModule mod_cache_socache.c&gt;
    CacheSocache shmcb
    CacheSocacheMaxSize 102400
    CacheEnable socache /
&lt;/IfModule&gt;
</code></pre>



<p class="wp-block-paragraph">Memory caches are faster but limited by available RAM and cleared on restart, so they&#8217;re best combined with disk caching as a fallback.</p>



<h2 class="wp-block-heading">Real-World Use Cases</h2>



<ul class="wp-block-list">
<li><strong>WordPress and CMS-driven sites</strong> – caching rendered HTML pages avoids expensive PHP/database calls on every request.</li>



<li><strong>Reverse proxy setups</strong> – Apache configured as a reverse proxy in front of application servers (Node.js, Python) benefits enormously from caching proxied responses.</li>



<li><strong>Static asset-heavy sites</strong> – portfolios, marketing sites, and documentation sites with lots of images and fonts see immediate load-time improvements.</li>



<li><strong>API gateways</strong> – caching read-heavy, infrequently changing API responses reduces backend load significantly.</li>
</ul>



<h2 class="wp-block-heading">Common Mistakes to Avoid</h2>



<ol class="wp-block-list">
<li><strong>Caching sensitive or personalized content</strong> – Never cache pages containing user-specific data (account pages, checkout) without proper <code>Vary</code> and <code>Cache-Control: private</code> headers.</li>



<li><strong>Setting cache lifetimes too long on HTML</strong> – HTML often changes; caching it for a year like static assets leads to stale content.</li>



<li><strong>Forgetting cache busting for updated assets</strong> – Use versioned filenames (e.g., <code>app.v2.js</code>) or query strings so browsers fetch new versions after deployment.</li>



<li><strong>Not excluding cookies from cached responses</strong> – Failing to set <code>CacheIgnoreHeaders Set-Cookie</code> can cause one user&#8217;s session cookie to be served to another.</li>



<li><strong>Ignoring cache invalidation</strong> – Have a plan (cache-clear command, TTL, or deployment hook) for purging stale cached content.</li>
</ol>



<h2 class="wp-block-heading">Security Best Practices</h2>



<ul class="wp-block-list">
<li>Never cache authenticated or personalized pages without explicit <code>private</code> cache directives.</li>



<li>Set <code>CacheIgnoreHeaders Set-Cookie</code> to avoid leaking session data between users.</li>



<li>Restrict write access to the cache directory to the Apache user only.</li>



<li>If using a CDN alongside Apache caching, ensure cache purge mechanisms are coordinated to avoid serving outdated or sensitive content.</li>
</ul>



<h2 class="wp-block-heading">Performance Optimization Tips</h2>



<ul class="wp-block-list">
<li>Combine caching with compression (see our companion Apache compression guide) — compress once, cache the compressed output.</li>



<li>Use long <code>max-age</code> values with immutable, versioned filenames for static assets.</li>



<li>Monitor cache hit ratios; a low hit ratio may mean your cache rules or TTLs need adjustment.</li>



<li>Consider a dedicated reverse proxy cache (like Varnish) in front of Apache for very high-traffic sites, using Apache&#8217;s caching as a secondary layer.</li>
</ul>



<h2 class="wp-block-heading">Troubleshooting</h2>



<p class="wp-block-paragraph"><strong>Cache not being used:</strong></p>



<ul class="wp-block-list">
<li>Confirm <code>mod_cache</code> and <code>mod_cache_disk</code> are enabled with <code>apache2ctl -M</code>.</li>



<li>Check that <code>CacheEnable</code> matches the correct URL path.</li>



<li>Ensure the cache directory has correct write permissions.</li>
</ul>



<p class="wp-block-paragraph"><strong>Stale content being served:</strong></p>



<ul class="wp-block-list">
<li>Lower <code>CacheMaxExpire</code> or manually clear the cache directory: <code>sudo rm -rf /var/cache/apache2/mod_cache_disk/*</code>.</li>



<li>Verify your application isn&#8217;t sending conflicting cache headers that override Apache&#8217;s rules.</li>
</ul>



<p class="wp-block-paragraph"><strong>Disk filling up:</strong></p>



<ul class="wp-block-list">
<li>Set <code>CacheMaxFileSize</code> to a reasonable limit and monitor disk usage; old cache entries are cleaned up automatically but a <code>CacheQuickHandler</code> misconfiguration can sometimes bypass expiry logic.</li>
</ul>



<h2 class="wp-block-heading">Frequently Asked Questions</h2>



<p class="wp-block-paragraph"><strong>What&#8217;s the difference between Expires and Cache-Control?</strong> <code>Expires</code> sets an absolute date/time; <code>Cache-Control</code> uses relative directives like <code>max-age</code> and offers more granular control (public/private, no-cache, immutable). Modern setups favor Cache-Control, with Expires as a fallback for older clients.</p>



<p class="wp-block-paragraph"><strong>Should I cache HTML pages?</strong> Static HTML can be cached briefly; dynamically generated HTML (like a CMS homepage) can be cached server-side with mod_cache but usually with a short TTL to balance freshness and performance.</p>



<p class="wp-block-paragraph"><strong>Does caching work with HTTPS?</strong> Yes, caching operates independently of TLS termination and works the same way over HTTPS.</p>



<p class="wp-block-paragraph"><strong>How do I clear the Apache cache?</strong> For disk caching, remove the contents of the <code>CacheRoot</code> directory. For browser caching, that&#8217;s controlled by the client, though cache-busting filenames force a refresh.</p>



<h2 class="wp-block-heading">Summary and Key Takeaways</h2>



<ul class="wp-block-list">
<li>Apache caching operates on two levels: browser caching (mod_expires, mod_headers) and server-side caching (mod_cache).</li>



<li>Set long cache lifetimes for static, versioned assets and short or no caching for dynamic HTML.</li>



<li>Use Cache-Control alongside Expires for the best browser compatibility.</li>



<li>Server-side caching with mod_cache_disk or mod_cache_socache reduces load on backend applications significantly.</li>



<li>Always exclude cookies and personalized content from cached responses.</li>



<li>Monitor cache hit rates and have a clear invalidation strategy for deployments.</li>
</ul>



<h2 class="wp-block-heading">References</h2>



<ul class="wp-block-list">
<li><a href="https://httpd.apache.org/docs/2.4/mod/mod_cache.html">Apache mod_cache Documentation</a></li>



<li><a href="https://httpd.apache.org/docs/2.4/mod/mod_expires.html">Apache mod_expires Documentation</a></li>



<li><a href="https://httpd.apache.org/docs/2.4/mod/mod_headers.html">Apache mod_headers Documentation</a></li>



<li><a href="https://httpd.apache.org/docs/2.4/caching.html">Apache Caching Guide</a></li>
</ul>
<p>The post <a href="https://awjunaid.com/apache/how-to-set-up-apache-for-content-caching/">How to Set Up Apache for Content Caching</a> appeared first on <a href="https://awjunaid.com">Abdul Wahab Junaid</a>.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://awjunaid.com/apache/how-to-set-up-apache-for-content-caching/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">5036</post-id>	</item>
		<item>
		<title>How to Enable Server-Side Scripting in Apache</title>
		<link>https://awjunaid.com/apache/how-to-enable-server-side-scripting-in-apache/</link>
					<comments>https://awjunaid.com/apache/how-to-enable-server-side-scripting-in-apache/?noamp=mobile#respond</comments>
		
		<dc:creator><![CDATA[Abdul Wahab Junaid]]></dc:creator>
		<pubDate>Mon, 11 Sep 2023 11:17:21 +0000</pubDate>
				<category><![CDATA[Apache]]></category>
		<category><![CDATA[apache]]></category>
		<guid isPermaLink="false">https://awjunaid.com/?p=5033</guid>

					<description><![CDATA[<p>When I first started managing servers, the idea of &#8220;server-side scripting&#8221; felt abstract until I actually enabled it&#8230;</p>
<p>The post <a href="https://awjunaid.com/apache/how-to-enable-server-side-scripting-in-apache/">How to Enable Server-Side Scripting in Apache</a> appeared first on <a href="https://awjunaid.com">Abdul Wahab Junaid</a>.</p>
]]></description>
										<content:encoded><![CDATA[
<p class="wp-block-paragraph">When I first started managing servers, the idea of &#8220;server-side scripting&#8221; felt abstract until I actually enabled it and watched a static HTML page turn into something that could talk to a database. In this guide, I&#8217;ll explain what server-side scripting means in the context of Apache, and walk through enabling it for the most common languages, along with the configuration details that trip people up.</p>



<h2 class="wp-block-heading">What Is Server-Side Scripting?</h2>



<p class="wp-block-paragraph">Server-side scripting means running code on the web server before the response is sent to the browser, rather than in the visitor&#8217;s browser (which is client-side scripting, like JavaScript running in Chrome). Common server-side languages include PHP, Python, Perl, and Ruby.</p>



<p class="wp-block-paragraph">Apache doesn&#8217;t execute these languages natively. Instead, it relies on modules or protocols to hand off requests to an interpreter:</p>



<ul class="wp-block-list">
<li><strong>mod_php</strong> – runs PHP directly inside the Apache process (legacy but still common)</li>



<li><strong>PHP-FPM via mod_proxy_fcgi</strong> – runs PHP as a separate FastCGI process pool (modern, recommended)</li>



<li><strong>mod_wsgi</strong> – runs Python web applications (Django, Flask)</li>



<li><strong>mod_perl</strong> – embeds a Perl interpreter in Apache</li>



<li><strong>mod_cgi / mod_cgid</strong> – runs any executable script as a CGI program, language-agnostic but slower</li>
</ul>



<p class="wp-block-paragraph">This guide covers the general mechanism and setup for CGI and FastCGI, since those are the foundation most language-specific guides (PHP, Python, Ruby) build on. For deep dives into specific stacks, see our companion articles on PHP and Python with Apache.</p>



<h2 class="wp-block-heading">Prerequisites</h2>



<ul class="wp-block-list">
<li>Apache 2.4+ with root/sudo access</li>



<li>A Linux server (Ubuntu/Debian or CentOS/RHEL examples below)</li>



<li>Basic familiarity with file permissions and the command line</li>



<li>The scripting language/interpreter you intend to use already installed (e.g., <code>php</code>, <code>python3</code>, <code>perl</code>)</li>
</ul>



<h2 class="wp-block-heading">Enabling CGI Scripting (The Foundation)</h2>



<p class="wp-block-paragraph">CGI (Common Gateway Interface) is the original method for server-side scripting and still useful for simple scripts or legacy systems.</p>



<h3 class="wp-block-heading">Step 1: Enable mod_cgi</h3>



<p class="wp-block-paragraph"><strong>Debian/Ubuntu (using the threaded mpm_event, use mod_cgid instead):</strong></p>



<pre class="wp-block-code"><code>sudo a2enmod cgid
sudo systemctl restart apache2
</code></pre>



<p class="wp-block-paragraph"><strong>CentOS/RHEL:</strong></p>



<pre class="wp-block-code"><code>LoadModule cgid_module modules/mod_cgid.so
</code></pre>



<h3 class="wp-block-heading">Step 2: Designate a CGI Directory</h3>



<p class="wp-block-paragraph">Edit your virtual host configuration:</p>



<pre class="wp-block-code"><code>&lt;VirtualHost *:80&gt;
    ServerName example.com
    DocumentRoot /var/www/example.com/public_html

    ScriptAlias /cgi-bin/ /var/www/example.com/cgi-bin/

    &lt;Directory "/var/www/example.com/cgi-bin"&gt;
        AllowOverride None
        Options +ExecCGI
        Require all granted
        AddHandler cgi-script .cgi .pl .py
    &lt;/Directory&gt;
&lt;/VirtualHost&gt;
</code></pre>



<h3 class="wp-block-heading">Step 3: Write a Test Script</h3>



<pre class="wp-block-code"><code>sudo mkdir -p /var/www/example.com/cgi-bin
sudo nano /var/www/example.com/cgi-bin/test.cgi
</code></pre>



<pre class="wp-block-code"><code>#!/usr/bin/perl
print "Content-type: text/html\n\n";
print "&lt;html&gt;&lt;body&gt;&lt;h1&gt;CGI is working!&lt;/h1&gt;&lt;/body&gt;&lt;/html&gt;";
</code></pre>



<p class="wp-block-paragraph">Make it executable:</p>



<pre class="wp-block-code"><code>sudo chmod +x /var/www/example.com/cgi-bin/test.cgi
</code></pre>



<p class="wp-block-paragraph">Restart Apache and visit <code>http://example.com/cgi-bin/test.cgi</code> to confirm it works.</p>



<h2 class="wp-block-heading">Enabling FastCGI (Modern, Recommended Approach)</h2>



<p class="wp-block-paragraph">FastCGI keeps interpreter processes running persistently instead of starting a new process per request, which is dramatically faster under load. This is the backbone of modern PHP-FPM and many Python deployments.</p>



<h3 class="wp-block-heading">Step 1: Enable Proxy and FastCGI Modules</h3>



<pre class="wp-block-code"><code>sudo a2enmod proxy
sudo a2enmod proxy_fcgi
sudo a2enmod setenvif
sudo systemctl restart apache2
</code></pre>



<h3 class="wp-block-heading">Step 2: Configure a FastCGI Handler</h3>



<p class="wp-block-paragraph">Example configuration pointing to a FastCGI process listening on a Unix socket (this pattern applies whether the backend is PHP-FPM, a Python WSGI server behind FastCGI, or similar):</p>



<pre class="wp-block-code"><code>&lt;FilesMatch "\.php$"&gt;
    SetHandler "proxy:unix:/run/php/php8.3-fpm.sock|fcgi://localhost/"
&lt;/FilesMatch&gt;
</code></pre>



<p class="wp-block-paragraph">This exact pattern is explored in depth in our PHP-specific guide, since PHP-FPM is the most common real-world use of FastCGI with Apache.</p>



<h2 class="wp-block-heading">Real-World Use Cases</h2>



<ul class="wp-block-list">
<li><strong>Contact forms and dynamic content</strong> – processing form submissions, sending emails, validating input.</li>



<li><strong>Content management systems</strong> – WordPress, Drupal, and Joomla all rely on server-side PHP execution.</li>



<li><strong>Web applications</strong> – Django/Flask (Python), Rails (Ruby), and similar frameworks require server-side execution to render pages and handle business logic.</li>



<li><strong>APIs and microservices</strong> – server-side scripts process requests, query databases, and return JSON responses.</li>



<li><strong>Legacy CGI scripts</strong> – some older systems (bioinformatics tools, internal admin scripts) still rely on classic CGI.</li>
</ul>



<h2 class="wp-block-heading">Common Mistakes to Avoid</h2>



<ol class="wp-block-list">
<li><strong>Forgetting +ExecCGI or the correct SetHandler</strong> – Without explicitly allowing script execution, Apache will serve the raw script source code instead of running it, which is both a functionality bug and a security risk.</li>



<li><strong>Incorrect file permissions</strong> – Scripts need execute permissions (<code>chmod +x</code>) for CGI; FastCGI sockets need correct ownership matching the Apache/FPM user.</li>



<li><strong>Mixing mod_php and PHP-FPM</strong> – Running both simultaneously can cause confusing behavior; pick one approach per site.</li>



<li><strong>Not restarting after enabling modules</strong> – Module changes require a full restart, not just a reload, in some cases.</li>



<li><strong>Leaving default/test scripts accessible in production</strong> – Test CGI scripts left in place can become an attack surface.</li>
</ol>



<h2 class="wp-block-heading">Security Best Practices</h2>



<ul class="wp-block-list">
<li><strong>Never enable ExecCGI or scripting handlers on directories that accept file uploads.</strong> If an attacker can upload a script and it lands in an executable-enabled directory, they can achieve remote code execution.</li>



<li><strong>Restrict scripting to specific, well-defined directories</strong> rather than enabling it site-wide with <code>Options +ExecCGI</code> in the DocumentRoot.</li>



<li><strong>Keep interpreters updated</strong> – PHP, Python, and Perl all receive regular security patches; outdated interpreters are a common attack vector.</li>



<li><strong>Run scripts with least privilege</strong> – use a dedicated, non-privileged system user for the web server and script execution, never root.</li>



<li><strong>Validate and sanitize all input</strong> in your scripts themselves; Apache&#8217;s job is just to execute the code, not to secure your application logic.</li>



<li><strong>Disable directory listing</strong> (<code>Options -Indexes</code>) so visitors can&#8217;t browse your cgi-bin or script directories directly.</li>
</ul>



<h2 class="wp-block-heading">Performance Optimization Tips</h2>



<ul class="wp-block-list">
<li>Prefer <strong>FastCGI/FPM over classic CGI</strong> wherever possible; process-per-request CGI is significantly slower under concurrent load.</li>



<li><strong>Tune process pool sizes</strong> (e.g., PHP-FPM&#8217;s <code>pm.max_children</code>) to match your server&#8217;s CPU and memory resources.</li>



<li><strong>Use opcode/bytecode caching</strong> where available (like PHP&#8217;s OPcache) to avoid recompiling scripts on every request.</li>



<li><strong>Combine with caching</strong> (see our Apache caching guide) for pages that don&#8217;t need to be regenerated on every single request.</li>
</ul>



<h2 class="wp-block-heading">Troubleshooting</h2>



<p class="wp-block-paragraph"><strong>&#8220;Internal Server Error&#8221; (500):</strong></p>



<ul class="wp-block-list">
<li>Check Apache&#8217;s error log: <code>sudo tail -f /var/log/apache2/error.log</code></li>



<li>Common causes: incorrect shebang line in the script, missing execute permission, or a syntax error in the script itself.</li>
</ul>



<p class="wp-block-paragraph"><strong>Script downloads instead of executing:</strong></p>



<ul class="wp-block-list">
<li>The handler isn&#8217;t correctly configured. Double-check <code>AddHandler</code> or <code>SetHandler</code> directives and that the relevant module is enabled.</li>
</ul>



<p class="wp-block-paragraph"><strong>&#8220;Premature end of script headers&#8221;:</strong></p>



<ul class="wp-block-list">
<li>The script didn&#8217;t output a valid <code>Content-type</code> header before any content. Every CGI script must print headers first, followed by a blank line, then content.</li>
</ul>



<p class="wp-block-paragraph"><strong>FastCGI socket connection errors:</strong></p>



<ul class="wp-block-list">
<li>Verify the backend process (like PHP-FPM) is running: <code>sudo systemctl status php8.3-fpm</code></li>



<li>Confirm the socket path in Apache&#8217;s config matches the actual socket file location and permissions.</li>
</ul>



<h2 class="wp-block-heading">Frequently Asked Questions</h2>



<p class="wp-block-paragraph"><strong>What&#8217;s the difference between CGI and FastCGI?</strong> CGI spawns a new process for every request, which is simple but slow. FastCGI keeps a pool of persistent processes running, dramatically reducing overhead per request.</p>



<p class="wp-block-paragraph"><strong>Do I need mod_cgi if I&#8217;m using PHP-FPM?</strong> No. PHP-FPM uses <code>mod_proxy_fcgi</code>, not classic CGI. mod_cgi is only needed for traditional CGI scripts (Perl, Python scripts written as standalone CGI programs, shell scripts, etc.).</p>



<p class="wp-block-paragraph"><strong>Is server-side scripting a security risk by itself?</strong> Not inherently, but misconfigured scripting handlers (enabling execution in upload directories, running as root, outdated interpreters) are among the most common causes of server compromise.</p>



<p class="wp-block-paragraph"><strong>Can I run multiple languages on the same Apache server?</strong> Yes. It&#8217;s common to run PHP-FPM for one site and a Python WSGI app (via mod_wsgi or a reverse-proxied app server) for another, all behind the same Apache instance using separate virtual hosts.</p>



<h2 class="wp-block-heading">Summary and Key Takeaways</h2>



<ul class="wp-block-list">
<li>Server-side scripting lets Apache execute code (PHP, Python, Perl, Ruby, etc.) to generate dynamic content before sending a response.</li>



<li>Classic CGI (mod_cgi/mod_cgid) is simple but slow; FastCGI (mod_proxy_fcgi) is the modern, performant standard.</li>



<li>Always scope scripting permissions to specific directories, never site-wide, and never in upload directories.</li>



<li>Keep interpreters patched and run them under a least-privilege user.</li>



<li>Check Apache&#8217;s error log first when scripts fail — most issues (permissions, missing headers, syntax errors) show up there immediately.</li>
</ul>



<h2 class="wp-block-heading">References</h2>



<ul class="wp-block-list">
<li><a href="https://httpd.apache.org/docs/2.4/mod/mod_cgi.html">Apache mod_cgi Documentation</a></li>



<li><a href="https://httpd.apache.org/docs/2.4/mod/mod_proxy_fcgi.html">Apache mod_proxy_fcgi Documentation</a></li>



<li><a href="https://httpd.apache.org/docs/2.4/howto/cgi.html">Apache Dynamic Content with CGI Guide</a></li>
</ul>



<p class="wp-block-paragraph"></p>
<p>The post <a href="https://awjunaid.com/apache/how-to-enable-server-side-scripting-in-apache/">How to Enable Server-Side Scripting in Apache</a> appeared first on <a href="https://awjunaid.com">Abdul Wahab Junaid</a>.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://awjunaid.com/apache/how-to-enable-server-side-scripting-in-apache/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">5033</post-id>	</item>
		<item>
		<title>How to Use Apache with Ruby on Rails</title>
		<link>https://awjunaid.com/apache/how-to-use-apache-with-ruby-on-rails/</link>
					<comments>https://awjunaid.com/apache/how-to-use-apache-with-ruby-on-rails/?noamp=mobile#respond</comments>
		
		<dc:creator><![CDATA[Abdul Wahab Junaid]]></dc:creator>
		<pubDate>Mon, 11 Sep 2023 11:15:52 +0000</pubDate>
				<category><![CDATA[Apache]]></category>
		<category><![CDATA[apache]]></category>
		<guid isPermaLink="false">https://awjunaid.com/?p=5030</guid>

					<description><![CDATA[<p>The first time I deployed a Rails app behind Apache, I assumed it would work like PHP —&#8230;</p>
<p>The post <a href="https://awjunaid.com/apache/how-to-use-apache-with-ruby-on-rails/">How to Use Apache with Ruby on Rails</a> appeared first on <a href="https://awjunaid.com">Abdul Wahab Junaid</a>.</p>
]]></description>
										<content:encoded><![CDATA[
<p class="wp-block-paragraph">The first time I deployed a Rails app behind Apache, I assumed it would work like PHP — drop files in a folder and go. It doesn&#8217;t, and figuring that out the hard way taught me a lot about how Rails actually runs in production. In this guide, I&#8217;ll walk through the correct way to pair Apache with Rails using a reverse proxy setup, which is how virtually all production Rails deployments work today.</p>



<h2 class="wp-block-heading">Understanding How Apache and Rails Work Together</h2>



<p class="wp-block-paragraph">Unlike PHP, Rails applications don&#8217;t run inside Apache. A Rails app is a standalone Ruby process (or set of processes) that runs independently, typically using an application server like <strong>Puma</strong> (the default since Rails 5), <strong>Unicorn</strong>, or <strong>Passenger</strong>.</p>



<p class="wp-block-paragraph">Apache&#8217;s role is to sit in front of that Rails process as a <strong>reverse proxy</strong>, forwarding incoming HTTP requests to the Rails app server and returning its responses to the client. Apache also handles tasks Rails shouldn&#8217;t have to worry about directly: SSL termination, serving static assets efficiently, load balancing across multiple app instances, and request logging.</p>



<p class="wp-block-paragraph">There are two common approaches:</p>



<ol class="wp-block-list">
<li><strong>mod_proxy + Puma/Unicorn</strong> – Apache proxies requests to a Ruby app server running as a separate process. This is the modern, most common approach.</li>



<li><strong>Passenger (mod_passenger)</strong> – An Apache module that manages the Rails application lifecycle directly, so you don&#8217;t need to manually run and monitor a separate app server process.</li>
</ol>



<p class="wp-block-paragraph">This guide covers both.</p>



<h2 class="wp-block-heading">Prerequisites</h2>



<ul class="wp-block-list">
<li>A Linux server with Apache 2.4+ installed</li>



<li>Ruby (2.7+, ideally 3.x) and Rails installed, along with a working Rails application</li>



<li>Root or sudo access</li>



<li>Basic familiarity with the command line and Rails&#8217; directory structure</li>



<li>Bundler installed (<code>gem install bundler</code>)</li>
</ul>



<h2 class="wp-block-heading">Approach 1: Apache as a Reverse Proxy to Puma</h2>



<h3 class="wp-block-heading">Step 1: Install Required Apache Modules</h3>



<pre class="wp-block-code"><code>sudo a2enmod proxy
sudo a2enmod proxy_http
sudo a2enmod headers
sudo systemctl restart apache2
</code></pre>



<h3 class="wp-block-heading">Step 2: Prepare Your Rails App</h3>



<p class="wp-block-paragraph">Inside your Rails app directory, make sure Puma is configured. Rails ships with a <code>config/puma.rb</code> file by default:</p>



<pre class="wp-block-code"><code># config/puma.rb
max_threads_count = ENV.fetch("RAILS_MAX_THREADS") { 5 }
min_threads_count = ENV.fetch("RAILS_MIN_THREADS") { max_threads_count }
threads min_threads_count, max_threads_count

port ENV.fetch("PORT") { 3000 }

environment ENV.fetch("RAILS_ENV") { "production" }

pidfile ENV.fetch("PIDFILE") { "tmp/pids/server.pid" }
</code></pre>



<p class="wp-block-paragraph">Precompile assets and set up the production environment:</p>



<pre class="wp-block-code"><code>RAILS_ENV=production bin/rails assets:precompile
RAILS_ENV=production bin/rails db:migrate
</code></pre>



<p class="wp-block-paragraph">Start Puma (in production, you&#8217;d typically run this under a process manager like systemd):</p>



<pre class="wp-block-code"><code>RAILS_ENV=production bundle exec puma -C config/puma.rb
</code></pre>



<h3 class="wp-block-heading">Step 3: Create a systemd Service for Puma</h3>



<p class="wp-block-paragraph">Rather than running Puma manually, create a persistent service:</p>



<pre class="wp-block-code"><code>sudo nano /etc/systemd/system/puma.service
</code></pre>



<pre class="wp-block-code"><code>&#91;Unit]
Description=Puma HTTP Server for MyRailsApp
After=network.target

&#91;Service]
Type=simple
User=deploy
WorkingDirectory=/var/www/myrailsapp
Environment=RAILS_ENV=production
ExecStart=/usr/local/rvm/bin/rvm-exec 3.2.0 bundle exec puma -C config/puma.rb
Restart=always

&#91;Install]
WantedBy=multi-user.target
</code></pre>



<p class="wp-block-paragraph">Enable and start it:</p>



<pre class="wp-block-code"><code>sudo systemctl daemon-reload
sudo systemctl enable puma
sudo systemctl start puma
</code></pre>



<h3 class="wp-block-heading">Step 4: Configure Apache as a Reverse Proxy</h3>



<p class="wp-block-paragraph">Create a virtual host:</p>



<pre class="wp-block-code"><code>&lt;VirtualHost *:80&gt;
    ServerName myrailsapp.com

    ProxyPreserveHost On
    ProxyPass / http://127.0.0.1:3000/
    ProxyPassReverse / http://127.0.0.1:3000/

    # Serve static assets directly from Apache for better performance
    Alias /assets /var/www/myrailsapp/public/assets
    &lt;Directory /var/www/myrailsapp/public/assets&gt;
        Require all granted
        Header set Cache-Control "public, max-age=31536000, immutable"
    &lt;/Directory&gt;

    ErrorLog ${APACHE_LOG_DIR}/myrailsapp_error.log
    CustomLog ${APACHE_LOG_DIR}/myrailsapp_access.log combined
&lt;/VirtualHost&gt;
</code></pre>



<p class="wp-block-paragraph">Enable the site and restart Apache:</p>



<pre class="wp-block-code"><code>sudo a2ensite myrailsapp.conf
sudo apachectl configtest
sudo systemctl restart apache2
</code></pre>



<h2 class="wp-block-heading">Approach 2: Using Passenger</h2>



<p class="wp-block-paragraph">Passenger integrates more tightly with Apache, managing the Rails process lifecycle for you, which simplifies deployment for many teams.</p>



<h3 class="wp-block-heading">Step 1: Install Passenger</h3>



<pre class="wp-block-code"><code>sudo apt install -y dirmngr gnupg apt-transport-https ca-certificates curl
curl https://oss-binaries.phusionpassenger.com/auto-software-signing-gpg-key.txt | sudo gpg --dearmor -o /usr/share/keyrings/phusion.gpg
sudo sh -c "echo 'deb &#91;signed-by=/usr/share/keyrings/phusion.gpg] https://oss-binaries.phusionpassenger.com/apt/passenger jammy main' &gt; /etc/apt/sources.list.d/passenger.list"
sudo apt update
sudo apt install -y libapache2-mod-passenger
</code></pre>



<h3 class="wp-block-heading">Step 2: Enable the Module</h3>



<pre class="wp-block-code"><code>sudo a2enmod passenger
sudo systemctl restart apache2
</code></pre>



<p class="wp-block-paragraph">Verify installation:</p>



<pre class="wp-block-code"><code>sudo /usr/bin/passenger-config validate-install
</code></pre>



<h3 class="wp-block-heading">Step 3: Configure the Virtual Host</h3>



<pre class="wp-block-code"><code>&lt;VirtualHost *:80&gt;
    ServerName myrailsapp.com
    DocumentRoot /var/www/myrailsapp/public

    &lt;Directory /var/www/myrailsapp/public&gt;
        Allow from all
        Options -MultiViews
        Require all granted
    &lt;/Directory&gt;

    PassengerRuby /usr/local/rvm/gems/ruby-3.2.0/wrappers/ruby
&lt;/VirtualHost&gt;
</code></pre>



<p class="wp-block-paragraph">With Passenger, there&#8217;s no need to manually manage a Puma process — Passenger starts and monitors Rails automatically based on the <code>DocumentRoot</code> pointing to <code>public/</code>.</p>



<h2 class="wp-block-heading">Enabling SSL/HTTPS</h2>



<p class="wp-block-paragraph">Regardless of which approach you use, secure your Rails app with SSL using Let&#8217;s Encrypt:</p>



<pre class="wp-block-code"><code>sudo apt install certbot python3-certbot-apache
sudo certbot --apache -d myrailsapp.com
</code></pre>



<p class="wp-block-paragraph">Certbot automatically updates your virtual host configuration to redirect HTTP to HTTPS and manages certificate renewal.</p>



<h2 class="wp-block-heading">Real-World Use Cases</h2>



<ul class="wp-block-list">
<li><strong>SaaS applications</strong> – Rails&#8217; convention-over-configuration approach makes it popular for building subscription-based web apps, with Apache handling SSL and load balancing in front.</li>



<li><strong>E-commerce platforms</strong> – Rails powers platforms like Shopify&#8217;s early architecture; Apache/Nginx reverse proxies are standard in front of such apps.</li>



<li><strong>Internal business tools</strong> – Rails is a common choice for rapid internal tool development, often deployed behind Apache in corporate environments already standardized on Apache.</li>



<li><strong>API backends</strong> – Rails in API-only mode paired with Apache as a proxy and rate-limiter for mobile or SPA frontends.</li>
</ul>



<h2 class="wp-block-heading">Common Mistakes to Avoid</h2>



<ol class="wp-block-list">
<li><strong>Forgetting to precompile assets</strong> – Running Rails in production without <code>assets:precompile</code> results in broken CSS/JS.</li>



<li><strong>Not setting RAILS_ENV=production</strong> – Running in development mode in production is a serious performance and security issue (verbose error pages leak information).</li>



<li><strong>Proxying static assets through Rails</strong> – Let Apache serve <code>/public/assets</code> directly rather than routing every asset request through the Ruby process.</li>



<li><strong>Missing ProxyPreserveHost</strong> – Without it, Rails may generate incorrect URLs (using the internal proxy address instead of the public domain).</li>



<li><strong>Running Puma as root</strong> – Always run application processes under a dedicated, non-privileged user.</li>
</ol>



<h2 class="wp-block-heading">Security Best Practices</h2>



<ul class="wp-block-list">
<li>Always run Rails in <code>production</code> mode with <code>config.force_ssl = true</code> in <code>config/environments/production.rb</code>.</li>



<li>Keep <code>SECRET_KEY_BASE</code> and other credentials out of version control; use Rails encrypted credentials or environment variables.</li>



<li>Set appropriate <code>Content-Security-Policy</code> and other security headers via <code>mod_headers</code> in Apache or Rails middleware.</li>



<li>Regularly run <code>bundle audit</code> to check for vulnerable gems.</li>



<li>Restrict direct access to port 3000 (or whatever Puma listens on) from outside the server — only Apache should be able to reach it, typically via binding Puma to <code>127.0.0.1</code>.</li>
</ul>



<h2 class="wp-block-heading">Performance Optimization Tips</h2>



<ul class="wp-block-list">
<li>Serve static assets and uploaded files directly through Apache rather than proxying them through Rails.</li>



<li>Use a CDN for assets in high-traffic production apps.</li>



<li>Tune Puma&#8217;s worker/thread counts based on available CPU cores (<code>WEB_CONCURRENCY</code> and <code>RAILS_MAX_THREADS</code>).</li>



<li>Enable HTTP/2 in Apache for faster asset delivery on the client side.</li>



<li>Combine with the caching techniques from our Apache caching guide for cacheable pages.</li>
</ul>



<h2 class="wp-block-heading">Troubleshooting</h2>



<p class="wp-block-paragraph"><strong>502 Bad Gateway:</strong></p>



<ul class="wp-block-list">
<li>Puma isn&#8217;t running or isn&#8217;t listening on the expected port. Check with <code>sudo systemctl status puma</code> and <code>curl http://127.0.0.1:3000</code>.</li>
</ul>



<p class="wp-block-paragraph"><strong>Assets not loading (404s for CSS/JS):</strong></p>



<ul class="wp-block-list">
<li>Confirm <code>assets:precompile</code> ran successfully and the Alias path in your Apache config matches the actual <code>public/assets</code> directory.</li>
</ul>



<p class="wp-block-paragraph"><strong>&#8220;We&#8217;re sorry, but something went wrong&#8221; generic error page:</strong></p>



<ul class="wp-block-list">
<li>Check <code>log/production.log</code> in your Rails app directory for the actual stack trace.</li>
</ul>



<p class="wp-block-paragraph"><strong>Passenger not starting the app:</strong></p>



<ul class="wp-block-list">
<li>Run <code>passenger-status</code> to see running application instances, and check Apache&#8217;s error log for Passenger-specific startup errors.</li>
</ul>



<h2 class="wp-block-heading">Frequently Asked Questions</h2>



<p class="wp-block-paragraph"><strong>Should I use Puma or Passenger?</strong> Puma with a manual reverse proxy gives more control and is the more common modern pattern, especially in containerized/cloud deployments. Passenger is simpler to set up and manages the process lifecycle for you, which some teams prefer for traditional VPS deployments.</p>



<p class="wp-block-paragraph"><strong>Can I run multiple Rails apps on one Apache server?</strong> Yes, using separate virtual hosts, each proxying to a different Puma instance on a different port (or Passenger handling each independently based on DocumentRoot).</p>



<p class="wp-block-paragraph"><strong>Is Nginx better than Apache for Rails?</strong> Nginx is more commonly seen in Rails tutorials, but Apache works equally well as a reverse proxy. The choice often comes down to existing infrastructure and team familiarity rather than a hard technical requirement.</p>



<p class="wp-block-paragraph"><strong>Do I need Apache at all, or can Puma serve requests directly?</strong> Puma can serve requests directly, but putting Apache in front provides SSL termination, static asset serving, load balancing, and additional security controls that are impractical to replicate in Puma alone.</p>



<h2 class="wp-block-heading">Summary and Key Takeaways</h2>



<ul class="wp-block-list">
<li>Rails doesn&#8217;t run inside Apache; it runs as a separate process (via Puma, Unicorn, or Passenger), with Apache acting as a reverse proxy.</li>



<li><code>mod_proxy</code> and <code>mod_proxy_http</code> are the key modules for proxying to Puma; Passenger offers a more integrated alternative.</li>



<li>Always run Rails in production mode, precompile assets, and serve static files directly through Apache.</li>



<li>Secure the deployment with SSL (Certbot/Let&#8217;s Encrypt), proper environment variables, and restricted access to the internal app server port.</li>



<li>Tune Puma&#8217;s thread/worker settings to match your server resources for the best performance.</li>
</ul>



<h2 class="wp-block-heading">References</h2>



<ul class="wp-block-list">
<li><a href="https://httpd.apache.org/docs/2.4/mod/mod_proxy.html">Apache mod_proxy Documentation</a></li>



<li><a href="https://httpd.apache.org/docs/2.4/mod/mod_proxy_http.html">Apache mod_proxy_http Documentation</a></li>



<li><a href="https://guides.rubyonrails.org/configuring.html">Ruby on Rails Guides: Configuring Rails Applications</a></li>



<li><a href="https://www.phusionpassenger.com/library/">Phusion Passenger Documentation</a></li>
</ul>
<p>The post <a href="https://awjunaid.com/apache/how-to-use-apache-with-ruby-on-rails/">How to Use Apache with Ruby on Rails</a> appeared first on <a href="https://awjunaid.com">Abdul Wahab Junaid</a>.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://awjunaid.com/apache/how-to-use-apache-with-ruby-on-rails/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">5030</post-id>	</item>
		<item>
		<title>How to install and configure Python with Apache</title>
		<link>https://awjunaid.com/apache/how-to-install-and-configure-python-with-apache/</link>
					<comments>https://awjunaid.com/apache/how-to-install-and-configure-python-with-apache/?noamp=mobile#respond</comments>
		
		<dc:creator><![CDATA[Abdul Wahab Junaid]]></dc:creator>
		<pubDate>Mon, 11 Sep 2023 11:14:04 +0000</pubDate>
				<category><![CDATA[Apache]]></category>
		<category><![CDATA[apache]]></category>
		<guid isPermaLink="false">https://awjunaid.com/?p=5027</guid>

					<description><![CDATA[<p>Python and Apache don&#8217;t naturally speak the same language out of the box, and I remember being confused&#8230;</p>
<p>The post <a href="https://awjunaid.com/apache/how-to-install-and-configure-python-with-apache/">How to install and configure Python with Apache</a> appeared first on <a href="https://awjunaid.com">Abdul Wahab Junaid</a>.</p>
]]></description>
										<content:encoded><![CDATA[
<p class="wp-block-paragraph">Python and Apache don&#8217;t naturally speak the same language out of the box, and I remember being confused early on about why my Flask app wouldn&#8217;t just &#8220;run&#8221; when I dropped it into the DocumentRoot. The missing piece is WSGI — the interface that lets Apache hand off requests to Python code. In this guide, I&#8217;ll walk through installing and configuring Python with Apache using mod_wsgi, the most common and reliable approach.</p>



<h2 class="wp-block-heading">Understanding How Python Works with Apache</h2>



<p class="wp-block-paragraph">Python web frameworks like Flask and Django follow the <strong>WSGI (Web Server Gateway Interface)</strong> specification, a standard that defines how a web server communicates with a Python application. Apache doesn&#8217;t understand WSGI natively, so it needs a bridge: <strong>mod_wsgi</strong>.</p>



<p class="wp-block-paragraph">mod_wsgi embeds a Python interpreter inside Apache (or runs it in separate daemon processes) and handles translating HTTP requests into calls your Python application understands, then converts the application&#8217;s response back into an HTTP response.</p>



<p class="wp-block-paragraph">There are two operating modes:</p>



<ul class="wp-block-list">
<li><strong>Embedded mode</strong> – Python runs inside Apache&#8217;s own worker processes. Simpler but less isolated.</li>



<li><strong>Daemon mode</strong> – Python runs in separate, dedicated processes managed by mod_wsgi. Recommended for production because it isolates your app from Apache&#8217;s process lifecycle and allows independent restarts.</li>
</ul>



<h2 class="wp-block-heading">Prerequisites</h2>



<ul class="wp-block-list">
<li>A Linux server with Apache 2.4+ installed</li>



<li>Root or sudo access</li>



<li>Python 3.8+ installed (<code>python3 --version</code>)</li>



<li>pip and venv available</li>



<li>A Python web application (Flask or Django example used below)</li>
</ul>



<h2 class="wp-block-heading">Step 1: Install Python and Development Tools</h2>



<p class="wp-block-paragraph"><strong>Debian/Ubuntu:</strong></p>



<pre class="wp-block-code"><code>sudo apt update
sudo apt install python3 python3-pip python3-venv python3-dev apache2 apache2-dev libapache2-mod-wsgi-py3
</code></pre>



<p class="wp-block-paragraph"><strong>CentOS/RHEL:</strong></p>



<pre class="wp-block-code"><code>sudo dnf install python3 python3-pip python3-devel httpd httpd-devel
sudo dnf install python3-mod_wsgi
</code></pre>



<h2 class="wp-block-heading">Step 2: Enable mod_wsgi</h2>



<p class="wp-block-paragraph"><strong>Debian/Ubuntu:</strong></p>



<pre class="wp-block-code"><code>sudo a2enmod wsgi
sudo systemctl restart apache2
</code></pre>



<p class="wp-block-paragraph">Verify it loaded:</p>



<pre class="wp-block-code"><code>apache2ctl -M | grep wsgi
</code></pre>



<p class="wp-block-paragraph"><strong>CentOS/RHEL:</strong></p>



<p class="wp-block-paragraph">Ensure this line exists in your Apache configuration:</p>



<pre class="wp-block-code"><code>LoadModule wsgi_module modules/mod_wsgi.so
</code></pre>



<p class="wp-block-paragraph">Then restart:</p>



<pre class="wp-block-code"><code>sudo systemctl restart httpd
</code></pre>



<h2 class="wp-block-heading">Step 3: Set Up a Python Virtual Environment</h2>



<p class="wp-block-paragraph">Isolating dependencies in a virtual environment is essential for production stability:</p>



<pre class="wp-block-code"><code>sudo mkdir -p /var/www/myflaskapp
cd /var/www/myflaskapp
python3 -m venv venv
source venv/bin/activate
pip install flask
</code></pre>



<h2 class="wp-block-heading">Step 4: Create a Simple Flask Application</h2>



<pre class="wp-block-code"><code>nano /var/www/myflaskapp/app.py
</code></pre>



<pre class="wp-block-code"><code>from flask import Flask

app = Flask(__name__)

@app.route("/")
def home():
    return "Hello from Flask running on Apache with mod_wsgi!"

if __name__ == "__main__":
    app.run()
</code></pre>



<h2 class="wp-block-heading">Step 5: Create the WSGI Entry Point</h2>



<pre class="wp-block-code"><code>nano /var/www/myflaskapp/myflaskapp.wsgi
</code></pre>



<pre class="wp-block-code"><code>#!/usr/bin/python3
import sys
import logging

logging.basicConfig(stream=sys.stderr)

sys.path.insert(0, "/var/www/myflaskapp/")

from app import app as application
</code></pre>



<p class="wp-block-paragraph">The variable must be named <code>application</code> — this is the convention mod_wsgi looks for.</p>



<h2 class="wp-block-heading">Step 6: Configure the Apache Virtual Host</h2>



<pre class="wp-block-code"><code>&lt;VirtualHost *:80&gt;
    ServerName myflaskapp.com

    WSGIDaemonProcess myflaskapp python-home=/var/www/myflaskapp/venv python-path=/var/www/myflaskapp
    WSGIProcessGroup myflaskapp
    WSGIScriptAlias / /var/www/myflaskapp/myflaskapp.wsgi

    &lt;Directory /var/www/myflaskapp&gt;
        Require all granted
    &lt;/Directory&gt;

    ErrorLog ${APACHE_LOG_DIR}/myflaskapp_error.log
    CustomLog ${APACHE_LOG_DIR}/myflaskapp_access.log combined
&lt;/VirtualHost&gt;
</code></pre>



<p class="wp-block-paragraph"><code>WSGIDaemonProcess</code> with <code>python-home</code> pointing to the virtual environment ensures Apache uses your isolated Python packages rather than the system-wide installation.</p>



<p class="wp-block-paragraph">Enable the site and restart:</p>



<pre class="wp-block-code"><code>sudo a2ensite myflaskapp.conf
sudo apachectl configtest
sudo systemctl restart apache2
</code></pre>



<p class="wp-block-paragraph">Visit <code>http://myflaskapp.com</code> — you should see the Flask response.</p>



<h2 class="wp-block-heading">Configuring Django with Apache</h2>



<p class="wp-block-paragraph">Django follows a similar pattern but with a slightly different WSGI entry point structure, since Django generates <code>wsgi.py</code> automatically.</p>



<h3 class="wp-block-heading">Step 1: Set Up the Django Project</h3>



<pre class="wp-block-code"><code>cd /var/www
python3 -m venv django_env
source django_env/bin/activate
pip install django
django-admin startproject myproject
</code></pre>



<h3 class="wp-block-heading">Step 2: Configure the Apache Virtual Host</h3>



<pre class="wp-block-code"><code>&lt;VirtualHost *:80&gt;
    ServerName mydjangoapp.com

    WSGIDaemonProcess myproject python-home=/var/www/django_env python-path=/var/www/myproject
    WSGIProcessGroup myproject
    WSGIScriptAlias / /var/www/myproject/myproject/wsgi.py

    &lt;Directory /var/www/myproject/myproject&gt;
        &lt;Files wsgi.py&gt;
            Require all granted
        &lt;/Files&gt;
    &lt;/Directory&gt;

    Alias /static/ /var/www/myproject/static/
    &lt;Directory /var/www/myproject/static&gt;
        Require all granted
    &lt;/Directory&gt;

    ErrorLog ${APACHE_LOG_DIR}/mydjangoapp_error.log
    CustomLog ${APACHE_LOG_DIR}/mydjangoapp_access.log combined
&lt;/VirtualHost&gt;
</code></pre>



<p class="wp-block-paragraph">Don&#8217;t forget to run <code>collectstatic</code> before going live:</p>



<pre class="wp-block-code"><code>python manage.py collectstatic
</code></pre>



<h2 class="wp-block-heading">Real-World Use Cases</h2>



<ul class="wp-block-list">
<li><strong>Data-driven web dashboards</strong> – Python&#8217;s data science ecosystem (pandas, matplotlib) pairs naturally with Flask/Django apps serving visualizations.</li>



<li><strong>Internal admin tools</strong> – Django&#8217;s built-in admin panel is a common choice for internal business applications deployed behind Apache.</li>



<li><strong>REST APIs</strong> – Flask or Django REST Framework backends serving mobile or JavaScript frontend applications.</li>



<li><strong>Machine learning model serving</strong> – Lightweight Flask apps wrapping ML models for inference, deployed behind Apache for SSL and load balancing.</li>
</ul>



<h2 class="wp-block-heading">Common Mistakes to Avoid</h2>



<ol class="wp-block-list">
<li><strong>Forgetting <code>python-home</code> in WSGIDaemonProcess</strong> – Without it, Apache uses the system Python, missing your virtual environment&#8217;s packages, leading to <code>ModuleNotFoundError</code>.</li>



<li><strong>Naming the WSGI callable incorrectly</strong> – It must be named <code>application</code>, not <code>app</code>, in the <code>.wsgi</code> file (even though your Flask variable can be named <code>app</code> inside <code>app.py</code>, the imported alias in the wsgi file must be <code>application</code>).</li>



<li><strong>Running the dev server in production</strong> – <code>app.run()</code> or Django&#8217;s <code>runserver</code> are not production-grade; always use mod_wsgi (or Gunicorn behind a proxy) instead.</li>



<li><strong>Mixing Python 2 and Python 3 mod_wsgi packages</strong> – Ensure you install <code>libapache2-mod-wsgi-py3</code> specifically, not the Python 2 version.</li>



<li><strong>Incorrect file permissions</strong> – The Apache user needs read access to your application directory and virtual environment.</li>
</ol>



<h2 class="wp-block-heading">Security Best Practices</h2>



<ul class="wp-block-list">
<li>Set <code>DEBUG = False</code> in Django&#8217;s settings for production; leaving debug mode on exposes sensitive stack traces and environment details.</li>



<li>Store secrets (<code>SECRET_KEY</code>, database credentials) in environment variables, not hardcoded in source files.</li>



<li>Restrict <code>ALLOWED_HOSTS</code> in Django to your actual domain(s).</li>



<li>Run WSGIDaemonProcess under a dedicated, non-privileged user rather than the default Apache user where isolation matters.</li>



<li>Keep Python, Flask/Django, and all dependencies updated; use <code>pip list --outdated</code> and tools like <code>pip-audit</code> regularly.</li>
</ul>



<h2 class="wp-block-heading">Performance Optimization Tips</h2>



<ul class="wp-block-list">
<li>Use <strong>daemon mode</strong> (not embedded mode) for better process isolation and easier scaling.</li>



<li>Tune <code>processes</code> and <code>threads</code> in <code>WSGIDaemonProcess</code> based on your workload:</li>
</ul>



<pre class="wp-block-code"><code>WSGIDaemonProcess myflaskapp python-home=/var/www/myflaskapp/venv processes=4 threads=2
</code></pre>



<ul class="wp-block-list">
<li>Serve static files directly through Apache (<code>Alias</code> directives) rather than through the Python application.</li>



<li>Combine with the compression and caching techniques from our other Apache guides.</li>



<li>Consider an alternative like Gunicorn or uWSGI behind Apache&#8217;s <code>mod_proxy</code> for very high-concurrency workloads, as some teams find this scales more predictably than mod_wsgi.</li>
</ul>



<h2 class="wp-block-heading">Troubleshooting</h2>



<p class="wp-block-paragraph"><strong>500 Internal Server Error with no clear message:</strong></p>



<ul class="wp-block-list">
<li>Check <code>error.log</code>: <code>sudo tail -f /var/log/apache2/myflaskapp_error.log</code>. mod_wsgi logs Python tracebacks here.</li>
</ul>



<p class="wp-block-paragraph"><strong>ModuleNotFoundError for installed packages:</strong></p>



<ul class="wp-block-list">
<li>Confirm <code>python-home</code> points to the correct virtual environment and that the package was installed inside that venv, not globally.</li>
</ul>



<p class="wp-block-paragraph"><strong>Static files return 404 in Django:</strong></p>



<ul class="wp-block-list">
<li>Run <code>collectstatic</code> and verify the <code>Alias /static/</code> path matches <code>STATIC_ROOT</code> in <code>settings.py</code>.</li>
</ul>



<p class="wp-block-paragraph"><strong>Changes to code not reflecting:</strong></p>



<ul class="wp-block-list">
<li>mod_wsgi daemon processes cache the application; restart Apache or touch the <code>.wsgi</code> file to force a reload:</li>
</ul>



<pre class="wp-block-code"><code>touch /var/www/myflaskapp/myflaskapp.wsgi
</code></pre>



<h2 class="wp-block-heading">Frequently Asked Questions</h2>



<p class="wp-block-paragraph"><strong>Do I need mod_wsgi, or can I use Gunicorn instead?</strong> Both work. mod_wsgi embeds Python directly into Apache&#8217;s process management. Gunicorn runs as a separate process, with Apache acting as a reverse proxy (similar to the Rails/Puma pattern). Gunicorn behind a proxy is increasingly common for containerized deployments; mod_wsgi remains popular for traditional VPS setups already standardized on Apache.</p>



<p class="wp-block-paragraph"><strong>Can I run multiple Python apps on the same Apache server?</strong> Yes, using separate <code>WSGIDaemonProcess</code> groups and virtual hosts, each with its own virtual environment and process isolation.</p>



<p class="wp-block-paragraph"><strong>Why does my app work with <code>python app.py</code> but not through Apache?</strong> The built-in Flask/Django development server behaves differently from mod_wsgi&#8217;s process model — typically the issue is either the <code>python-home</code> path, file permissions, or a missing environment variable that was set locally but not in Apache&#8217;s environment.</p>



<p class="wp-block-paragraph"><strong>Is mod_wsgi actively maintained?</strong> Yes, mod_wsgi continues to be maintained and is a stable, production-proven choice for serving Python WSGI applications through Apache.</p>



<h2 class="wp-block-heading">Summary and Key Takeaways</h2>



<ul class="wp-block-list">
<li>Python applications communicate with Apache through the WSGI interface, bridged by mod_wsgi.</li>



<li>Always use a virtual environment and point <code>WSGIDaemonProcess</code> to it via <code>python-home</code>.</li>



<li>Daemon mode is preferred over embedded mode for production isolation and stability.</li>



<li>Django requires additional static file configuration (<code>collectstatic</code> + <code>Alias</code>) that Flask doesn&#8217;t need out of the box.</li>



<li>Never run Flask&#8217;s or Django&#8217;s built-in development server in production — mod_wsgi (or Gunicorn behind a proxy) is the correct approach.</li>



<li>Check the Apache error log first whenever something goes wrong; Python tracebacks are logged there.</li>
</ul>



<h2 class="wp-block-heading">References</h2>



<ul class="wp-block-list">
<li><a href="https://modwsgi.readthedocs.io/">Apache mod_wsgi Documentation</a></li>



<li><a href="https://httpd.apache.org/docs/2.4/">Apache HTTP Server Documentation</a></li>



<li><a href="https://docs.djangoproject.com/en/stable/howto/deployment/wsgi/modwsgi/">Django Deployment with mod_wsgi</a></li>



<li><a href="https://flask.palletsprojects.com/en/latest/deploying/">Flask Deployment Options</a></li>
</ul>



<p class="wp-block-paragraph"></p>
<p>The post <a href="https://awjunaid.com/apache/how-to-install-and-configure-python-with-apache/">How to install and configure Python with Apache</a> appeared first on <a href="https://awjunaid.com">Abdul Wahab Junaid</a>.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://awjunaid.com/apache/how-to-install-and-configure-python-with-apache/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">5027</post-id>	</item>
		<item>
		<title>How to Install and Configure PHP on Apache</title>
		<link>https://awjunaid.com/apache/how-to-install-and-configure-php-on-apache/</link>
					<comments>https://awjunaid.com/apache/how-to-install-and-configure-php-on-apache/?noamp=mobile#respond</comments>
		
		<dc:creator><![CDATA[Abdul Wahab Junaid]]></dc:creator>
		<pubDate>Mon, 11 Sep 2023 11:12:03 +0000</pubDate>
				<category><![CDATA[Apache]]></category>
		<category><![CDATA[apache]]></category>
		<guid isPermaLink="false">https://awjunaid.com/?p=5024</guid>

					<description><![CDATA[<p>PHP and Apache have been running the web together for decades, and it&#8217;s still one of the most&#8230;</p>
<p>The post <a href="https://awjunaid.com/apache/how-to-install-and-configure-php-on-apache/">How to Install and Configure PHP on Apache</a> appeared first on <a href="https://awjunaid.com">Abdul Wahab Junaid</a>.</p>
]]></description>
										<content:encoded><![CDATA[
<p class="wp-block-paragraph">PHP and Apache have been running the web together for decades, and it&#8217;s still one of the most common stacks I set up for clients, whether it&#8217;s a fresh WordPress install or a custom application. In this guide, I&#8217;ll walk through installing PHP, connecting it to Apache the modern way, and configuring it properly for production use.</p>



<h2 class="wp-block-heading">Understanding How PHP Works with Apache</h2>



<p class="wp-block-paragraph">There are two main ways to run PHP alongside Apache:</p>



<ol class="wp-block-list">
<li><strong>mod_php</strong> – PHP runs as an Apache module, embedded directly in Apache&#8217;s worker processes. It&#8217;s simple to set up but ties PHP&#8217;s performance and stability to Apache&#8217;s process model, and only works with the older <code>mpm_prefork</code> module.</li>



<li><strong>PHP-FPM (FastCGI Process Manager)</strong> – PHP runs as an independent process pool, and Apache communicates with it via FastCGI (<code>mod_proxy_fcgi</code>). This is the modern, recommended approach: it&#8217;s faster, more memory-efficient, and works with Apache&#8217;s high-performance <code>mpm_event</code> module.</li>
</ol>



<p class="wp-block-paragraph">This guide focuses primarily on PHP-FPM, since it&#8217;s the standard for new deployments, with a note on mod_php for completeness.</p>



<h2 class="wp-block-heading">Prerequisites</h2>



<ul class="wp-block-list">
<li>A Linux server with Apache 2.4+ installed</li>



<li>Root or sudo access</li>



<li>Basic familiarity with the command line</li>
</ul>



<h2 class="wp-block-heading">Installing PHP-FPM and Connecting It to Apache</h2>



<h3 class="wp-block-heading">Step 1: Install PHP-FPM</h3>



<p class="wp-block-paragraph"><strong>Debian/Ubuntu:</strong></p>



<pre class="wp-block-code"><code>sudo apt update
sudo apt install php-fpm php-mysql php-curl php-gd php-mbstring php-xml php-zip
</code></pre>



<p class="wp-block-paragraph"><strong>CentOS/RHEL:</strong></p>



<pre class="wp-block-code"><code>sudo dnf install epel-release
sudo dnf install php-fpm php-mysqlnd php-curl php-gd php-mbstring php-xml php-zip
</code></pre>



<p class="wp-block-paragraph">Check the installed version:</p>



<pre class="wp-block-code"><code>php -v
</code></pre>



<h3 class="wp-block-heading">Step 2: Switch Apache to mpm_event and Enable FastCGI Proxy Modules</h3>



<pre class="wp-block-code"><code>sudo a2dismod mpm_prefork
sudo a2enmod mpm_event
sudo a2enmod proxy_fcgi
sudo a2enmod setenvif
sudo a2enconf php8.3-fpm
sudo systemctl restart apache2
</code></pre>



<p class="wp-block-paragraph">(Replace <code>8.3</code> with your installed PHP version — check with <code>php -v</code>.)</p>



<h3 class="wp-block-heading">Step 3: Start and Enable PHP-FPM</h3>



<pre class="wp-block-code"><code>sudo systemctl start php8.3-fpm
sudo systemctl enable php8.3-fpm
sudo systemctl status php8.3-fpm
</code></pre>



<h3 class="wp-block-heading">Step 4: Configure the Virtual Host</h3>



<pre class="wp-block-code"><code>&lt;VirtualHost *:80&gt;
    ServerName example.com
    DocumentRoot /var/www/example.com/public_html

    &lt;Directory /var/www/example.com/public_html&gt;
        AllowOverride All
        Require all granted
    &lt;/Directory&gt;

    &lt;FilesMatch \.php$&gt;
        SetHandler "proxy:unix:/run/php/php8.3-fpm.sock|fcgi://localhost/"
    &lt;/FilesMatch&gt;

    ErrorLog ${APACHE_LOG_DIR}/example.com_error.log
    CustomLog ${APACHE_LOG_DIR}/example.com_access.log combined
&lt;/VirtualHost&gt;
</code></pre>



<p class="wp-block-paragraph">Enable the site and restart Apache:</p>



<pre class="wp-block-code"><code>sudo a2ensite example.com.conf
sudo apachectl configtest
sudo systemctl restart apache2
</code></pre>



<h3 class="wp-block-heading">Step 5: Test PHP Is Working</h3>



<pre class="wp-block-code"><code>sudo nano /var/www/example.com/public_html/info.php
</code></pre>



<pre class="wp-block-code"><code>&lt;?php
phpinfo();
</code></pre>



<p class="wp-block-paragraph">Visit <code>http://example.com/info.php</code> in a browser. You should see the PHP configuration page. <strong>Delete this file after testing</strong> — it exposes detailed server information that shouldn&#8217;t be public.</p>



<pre class="wp-block-code"><code>sudo rm /var/www/example.com/public_html/info.php
</code></pre>



<h2 class="wp-block-heading">Alternative: Installing mod_php (Legacy Approach)</h2>



<p class="wp-block-paragraph">If you have a specific reason to use mod_php (some legacy applications assume it), here&#8217;s the setup:</p>



<pre class="wp-block-code"><code>sudo apt install libapache2-mod-php
sudo a2dismod mpm_event
sudo a2enmod mpm_prefork
sudo a2enmod php8.3
sudo systemctl restart apache2
</code></pre>



<p class="wp-block-paragraph">Note that mod_php requires <code>mpm_prefork</code>, which handles one request per process/thread and generally performs worse under high concurrency than <code>mpm_event</code> with PHP-FPM.</p>



<h2 class="wp-block-heading">Configuring php.ini for Production</h2>



<p class="wp-block-paragraph">Locate your php.ini file:</p>



<pre class="wp-block-code"><code>php --ini
</code></pre>



<p class="wp-block-paragraph">For FPM, it&#8217;s typically at <code>/etc/php/8.3/fpm/php.ini</code>. Key production settings to review:</p>



<pre class="wp-block-code"><code>; Hide PHP version from response headers (security)
expose_php = Off

; Disable dangerous functions if not needed
disable_functions = exec,passthru,shell_exec,system,proc_open,popen

; Limit file upload size appropriately
upload_max_filesize = 20M
post_max_size = 20M

; Set a reasonable execution time limit
max_execution_time = 30

; Enable OPcache for performance
opcache.enable = 1
opcache.memory_consumption = 128
opcache.max_accelerated_files = 10000
opcache.revalidate_freq = 60

; Error handling - never display errors in production
display_errors = Off
log_errors = On
error_log = /var/log/php/error.log
</code></pre>



<p class="wp-block-paragraph">After editing, restart PHP-FPM:</p>



<pre class="wp-block-code"><code>sudo systemctl restart php8.3-fpm
</code></pre>



<h2 class="wp-block-heading">Tuning PHP-FPM Process Management</h2>



<p class="wp-block-paragraph">Edit the pool configuration, typically at <code>/etc/php/8.3/fpm/pool.d/www.conf</code>:</p>



<pre class="wp-block-code"><code>pm = dynamic
pm.max_children = 50
pm.start_servers = 5
pm.min_spare_servers = 5
pm.max_spare_servers = 10
pm.max_requests = 500
</code></pre>



<ul class="wp-block-list">
<li><code>pm.max_children</code> should be calculated based on available RAM divided by average memory per PHP process.</li>



<li><code>pm.max_requests</code> recycles worker processes periodically, helping mitigate memory leaks in long-running scripts.</li>
</ul>



<p class="wp-block-paragraph">Restart after changes:</p>



<pre class="wp-block-code"><code>sudo systemctl restart php8.3-fpm
</code></pre>



<h2 class="wp-block-heading">Real-World Use Cases</h2>



<ul class="wp-block-list">
<li><strong>WordPress, Drupal, Joomla</strong> – the vast majority of CMS platforms run on PHP and Apache.</li>



<li><strong>E-commerce platforms</strong> – Magento and WooCommerce (WordPress-based) rely on this stack heavily.</li>



<li><strong>Custom web applications</strong> – frameworks like Laravel and Symfony run on PHP-FPM behind Apache in most production deployments.</li>



<li><strong>Shared hosting environments</strong> – many hosting providers still default to Apache + PHP-FPM for its balance of compatibility and performance.</li>
</ul>



<h2 class="wp-block-heading">Common Mistakes to Avoid</h2>



<ol class="wp-block-list">
<li><strong>Leaving phpinfo() accessible in production</strong> – This exposes internal paths, loaded modules, and configuration details useful to attackers.</li>



<li><strong>Using mod_php with mpm_event</strong> – This combination doesn&#8217;t work; mod_php requires <code>mpm_prefork</code>.</li>



<li><strong>Not matching the socket path in Apache config to the actual PHP-FPM socket</strong> – A common cause of 502 errors after a PHP version upgrade.</li>



<li><strong>Ignoring pm.max_children tuning</strong> – Leaving it at low defaults on a high-traffic site causes requests to queue and time out.</li>



<li><strong>Displaying errors in production</strong> – <code>display_errors = On</code> leaks file paths and code structure to visitors; always log errors instead.</li>
</ol>



<h2 class="wp-block-heading">Security Best Practices</h2>



<ul class="wp-block-list">
<li>Set <code>expose_php = Off</code> to avoid advertising your PHP version in HTTP headers.</li>



<li>Disable unused, dangerous PHP functions (<code>exec</code>, <code>shell_exec</code>, <code>system</code>) unless your application specifically needs them.</li>



<li>Keep PHP updated — end-of-life PHP versions no longer receive security patches and are a common attack vector.</li>



<li>Run PHP-FPM pools under a dedicated, non-privileged user, separate from Apache&#8217;s user where possible, for better isolation between sites on shared servers.</li>



<li>Set strict <code>upload_max_filesize</code> and validate uploaded file types at the application level, not just via PHP config.</li>



<li>Use <code>open_basedir</code> restrictions to confine PHP scripts to specific directories, limiting the blast radius if a script is compromised.</li>
</ul>



<h2 class="wp-block-heading">Performance Optimization Tips</h2>



<ul class="wp-block-list">
<li>Always enable <strong>OPcache</strong> — it caches compiled PHP bytecode, dramatically reducing CPU usage on repeated requests.</li>



<li>Use <strong>PHP-FPM with mpm_event</strong> rather than mod_php with mpm_prefork for better concurrency handling.</li>



<li>Combine with the compression and caching techniques from our other Apache guides for full-stack performance gains.</li>



<li>Monitor PHP-FPM&#8217;s status page (<code>pm.status_path</code>) to track pool utilization and tune <code>pm.max_children</code> based on real traffic data.</li>



<li>Consider a PHP-level object cache (like Redis or Memcached) for applications with expensive, repeated database queries.</li>
</ul>



<h2 class="wp-block-heading">Troubleshooting</h2>



<p class="wp-block-paragraph"><strong>502 Bad Gateway:</strong></p>



<ul class="wp-block-list">
<li>PHP-FPM isn&#8217;t running, or the socket path in Apache&#8217;s config doesn&#8217;t match the actual socket. Check with <code>sudo systemctl status php8.3-fpm</code> and verify the path in <code>/etc/php/8.3/fpm/pool.d/www.conf</code> (<code>listen = /run/php/php8.3-fpm.sock</code>).</li>
</ul>



<p class="wp-block-paragraph"><strong>White screen with no error message:</strong></p>



<ul class="wp-block-list">
<li><code>display_errors</code> is off (correct for production) but nothing is being logged. Check <code>error_log</code> in php.ini and Apache&#8217;s error log.</li>
</ul>



<p class="wp-block-paragraph"><strong>&#8220;File not found&#8221; errors for .php files:</strong></p>



<ul class="wp-block-list">
<li>Verify <code>DirectoryIndex</code> includes <code>index.php</code> in your Apache config, and that the <code>FilesMatch</code> handler block is correctly scoped to the virtual host.</li>
</ul>



<p class="wp-block-paragraph"><strong>High memory usage:</strong></p>



<ul class="wp-block-list">
<li>Review <code>pm.max_children</code> — too high a value on limited RAM causes the OOM killer to terminate processes unpredictably.</li>
</ul>



<h2 class="wp-block-heading">Frequently Asked Questions</h2>



<p class="wp-block-paragraph"><strong>Should I use PHP-FPM or mod_php for a new project?</strong> PHP-FPM, in almost all cases. It&#8217;s faster, more memory-efficient, and compatible with Apache&#8217;s modern <code>mpm_event</code> MPM.</p>



<p class="wp-block-paragraph"><strong>How do I run multiple PHP versions on the same server?</strong> Install multiple PHP-FPM versions side by side (e.g., <code>php8.1-fpm</code> and <code>php8.3-fpm</code>), each with its own socket, and point different virtual hosts to the appropriate socket in their <code>FilesMatch</code> block.</p>



<p class="wp-block-paragraph"><strong>Do I need to restart Apache after changing php.ini?</strong> No — restart PHP-FPM instead (<code>sudo systemctl restart php8.3-fpm</code>), since PHP-FPM manages its own processes independently of Apache.</p>



<p class="wp-block-paragraph"><strong>Is PHP still relevant in 2026?</strong> Yes. PHP continues to power a substantial share of the web, particularly through WordPress and other CMS platforms, and modern PHP (8.x) with OPcache and PHP-FPM is a solid, performant choice.</p>



<h2 class="wp-block-heading">Summary and Key Takeaways</h2>



<ul class="wp-block-list">
<li>PHP-FPM with <code>mod_proxy_fcgi</code> and Apache&#8217;s <code>mpm_event</code> is the modern, recommended way to run PHP on Apache.</li>



<li>mod_php still works but requires the older, less concurrent <code>mpm_prefork</code> MPM.</li>



<li>Always disable <code>display_errors</code> and <code>expose_php</code> in production, and remove test <code>phpinfo()</code> files immediately after use.</li>



<li>Enable OPcache and tune <code>pm.max_children</code> based on your server&#8217;s available memory for the best performance.</li>



<li>Check PHP-FPM&#8217;s status and the Apache error log first when diagnosing 502 errors or blank pages.</li>
</ul>



<h2 class="wp-block-heading">References</h2>



<ul class="wp-block-list">
<li><a href="https://httpd.apache.org/docs/2.4/mod/mod_proxy_fcgi.html">Apache mod_proxy_fcgi Documentation</a></li>



<li><a href="https://www.php.net/manual/en/install.fpm.php">PHP: PHP-FPM Documentation</a></li>



<li><a href="https://www.php.net/manual/en/book.opcache.php">PHP: OPcache Documentation</a></li>



<li><a href="https://httpd.apache.org/docs/2.4/">Apache HTTP Server Documentation</a></li>
</ul>
<p>The post <a href="https://awjunaid.com/apache/how-to-install-and-configure-php-on-apache/">How to Install and Configure PHP on Apache</a> appeared first on <a href="https://awjunaid.com">Abdul Wahab Junaid</a>.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://awjunaid.com/apache/how-to-install-and-configure-php-on-apache/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">5024</post-id>	</item>
		<item>
		<title>How to Set Up Apache for Serving Dynamic Content</title>
		<link>https://awjunaid.com/apache/how-to-set-up-apache-for-serving-dynamic-content/</link>
					<comments>https://awjunaid.com/apache/how-to-set-up-apache-for-serving-dynamic-content/?noamp=mobile#respond</comments>
		
		<dc:creator><![CDATA[Abdul Wahab Junaid]]></dc:creator>
		<pubDate>Mon, 11 Sep 2023 11:08:10 +0000</pubDate>
				<category><![CDATA[Apache]]></category>
		<category><![CDATA[apache]]></category>
		<guid isPermaLink="false">https://awjunaid.com/?p=5019</guid>

					<description><![CDATA[<p>When I first started managing web servers, I assumed Apache was just for dropping HTML files into a&#8230;</p>
<p>The post <a href="https://awjunaid.com/apache/how-to-set-up-apache-for-serving-dynamic-content/">How to Set Up Apache for Serving Dynamic Content</a> appeared first on <a href="https://awjunaid.com">Abdul Wahab Junaid</a>.</p>
]]></description>
										<content:encoded><![CDATA[
<p class="wp-block-paragraph">When I first started managing web servers, I assumed Apache was just for dropping HTML files into a folder and calling it a day. It took me a while to realize just how much dynamic content Apache can handle once you know which modules to enable and how to wire them up. In this post, I&#8217;ll walk you through everything I&#8217;ve learned about configuring Apache to serve dynamic content — from PHP and Python to Node.js backends — along with the prerequisites, configuration steps, and a few hard-earned troubleshooting tips.</p>



<h2 class="wp-block-heading">What Does &#8220;Dynamic Content&#8221; Actually Mean?</h2>



<p class="wp-block-paragraph">Static content is simple: a browser requests a file, and Apache hands it over exactly as it&#8217;s stored on disk. Dynamic content is different. Instead of serving a file as-is, Apache runs a script or forwards the request to an application, which generates the response on the fly — often pulling data from a database, session, or API.</p>



<p class="wp-block-paragraph">Common examples of dynamic content include:</p>



<ul class="wp-block-list">
<li>PHP-driven websites (WordPress, Laravel, custom PHP apps)</li>



<li>Python applications (Flask, Django) run through WSGI</li>



<li>Node.js or Java applications proxied through Apache</li>



<li>CGI or Perl scripts (an older but still-used approach)</li>
</ul>



<h2 class="wp-block-heading">Why Use Apache for Dynamic Content?</h2>



<p class="wp-block-paragraph">You might be wondering why not just run your application server directly and skip Apache altogether. In my experience, Apache adds a lot of value as a front-facing layer:</p>



<ul class="wp-block-list">
<li><strong>Battle-tested stability</strong> — Apache has been handling production traffic for decades.</li>



<li><strong>Flexible module system</strong> — you can bolt on exactly the functionality you need.</li>



<li><strong>SSL termination</strong> — handling HTTPS at the Apache layer keeps your application code simpler.</li>



<li><strong>Access control and rewriting</strong> — <code>.htaccess</code> and <code>mod_rewrite</code> give you fine-grained control over how requests are handled before they even reach your app.</li>



<li><strong>Reverse proxy capabilities</strong> — Apache can sit in front of application servers written in any language.</li>
</ul>



<h2 class="wp-block-heading">Prerequisites</h2>



<p class="wp-block-paragraph">Before diving into configuration, make sure you have:</p>



<ol class="wp-block-list">
<li>A Linux server (I&#8217;ll use Ubuntu/Debian syntax here, with notes for CentOS/RHEL)</li>



<li>Apache installed (<code>apache2</code> on Debian-based systems, <code>httpd</code> on RHEL-based systems)</li>



<li>Root or sudo access</li>



<li>Basic familiarity with editing configuration files</li>



<li>The scripting language or runtime you plan to use already installed (PHP, Python, Node.js, etc.)</li>
</ol>



<h2 class="wp-block-heading">Step 1: Install Apache</h2>



<p class="wp-block-paragraph">On Ubuntu/Debian:</p>



<pre class="wp-block-code"><code>sudo apt update
sudo apt install apache2 -y
</code></pre>



<p class="wp-block-paragraph">On CentOS/RHEL:</p>



<pre class="wp-block-code"><code>sudo yum install httpd -y
sudo systemctl enable httpd
sudo systemctl start httpd
</code></pre>



<p class="wp-block-paragraph">Confirm it&#8217;s running:</p>



<pre class="wp-block-code"><code>sudo systemctl status apache2
</code></pre>



<h2 class="wp-block-heading">Step 2: Serving PHP Dynamic Content</h2>



<p class="wp-block-paragraph">PHP is probably the most common dynamic content use case for Apache, so I&#8217;ll start there.</p>



<p class="wp-block-paragraph">Install PHP and the Apache PHP module:</p>



<pre class="wp-block-code"><code>sudo apt install php libapache2-mod-php php-mysql -y
</code></pre>



<p class="wp-block-paragraph">Apache automatically enables <code>mod_php</code> on Debian-based installs. Verify it&#8217;s active:</p>



<pre class="wp-block-code"><code>apache2ctl -M | grep php
</code></pre>



<p class="wp-block-paragraph">Create a test file:</p>



<pre class="wp-block-code"><code>sudo nano /var/www/html/info.php
</code></pre>



<pre class="wp-block-code"><code>&lt;?php
phpinfo();
?&gt;
</code></pre>



<p class="wp-block-paragraph">Visit <code>http://your-server-ip/info.php</code> in a browser. If you see the PHP info page, dynamic PHP content is working. Remove this file once you&#8217;ve confirmed it works — leaving <code>phpinfo()</code> publicly accessible is a security risk.</p>



<pre class="wp-block-code"><code>sudo rm /var/www/html/info.php
</code></pre>



<h2 class="wp-block-heading">Step 3: Serving Python Applications with mod_wsgi</h2>



<p class="wp-block-paragraph">For Python apps (Flask/Django), the standard approach is <code>mod_wsgi</code>.</p>



<pre class="wp-block-code"><code>sudo apt install libapache2-mod-wsgi-py3 -y
sudo a2enmod wsgi
</code></pre>



<p class="wp-block-paragraph">Here&#8217;s a minimal Flask app I use for testing:</p>



<pre class="wp-block-code"><code># /var/www/myapp/app.py
def application(environ, start_response):
    status = '200 OK'
    output = b'Hello from dynamic Python content!'
    response_headers = &#91;('Content-type', 'text/plain'),
                         ('Content-Length', str(len(output)))]
    start_response(status, response_headers)
    return &#91;output]
</code></pre>



<p class="wp-block-paragraph">Then configure a virtual host:</p>



<pre class="wp-block-code"><code>&lt;VirtualHost *:80&gt;
    ServerName python.example.com
    WSGIDaemonProcess myapp threads=5
    WSGIScriptAlias / /var/www/myapp/app.py

    &lt;Directory /var/www/myapp&gt;
        WSGIProcessGroup myapp
        WSGIApplicationGroup %{GLOBAL}
        Require all granted
    &lt;/Directory&gt;
&lt;/VirtualHost&gt;
</code></pre>



<p class="wp-block-paragraph">Enable the site and restart:</p>



<pre class="wp-block-code"><code>sudo a2ensite python-app.conf
sudo systemctl restart apache2
</code></pre>



<h2 class="wp-block-heading">Step 4: Serving Node.js Apps via Reverse Proxy</h2>



<p class="wp-block-paragraph">Apache doesn&#8217;t run Node.js directly, but it can proxy requests to a Node process running on a local port. This is my preferred method for Node-based dynamic content.</p>



<p class="wp-block-paragraph">Enable the proxy modules:</p>



<pre class="wp-block-code"><code>sudo a2enmod proxy proxy_http
</code></pre>



<p class="wp-block-paragraph">Configure the virtual host:</p>



<pre class="wp-block-code"><code>&lt;VirtualHost *:80&gt;
    ServerName node.example.com
    ProxyPreserveHost On
    ProxyPass / http://127.0.0.1:3000/
    ProxyPassReverse / http://127.0.0.1:3000/
&lt;/VirtualHost&gt;
</code></pre>



<p class="wp-block-paragraph">Restart Apache:</p>



<pre class="wp-block-code"><code>sudo systemctl restart apache2
</code></pre>



<h2 class="wp-block-heading">Step 5: Directory and Permissions Configuration</h2>



<p class="wp-block-paragraph">Dynamic content often needs write access for logs, uploads, or cache directories. I always double-check ownership:</p>



<pre class="wp-block-code"><code>sudo chown -R www-data:www-data /var/www/myapp
sudo chmod -R 755 /var/www/myapp
</code></pre>



<p class="wp-block-paragraph">Avoid <code>chmod 777</code> — it&#8217;s a common shortcut that opens serious security holes.</p>



<h2 class="wp-block-heading">Real-World Use Cases</h2>



<ul class="wp-block-list">
<li><strong>E-commerce platforms</strong> running PHP-based Magento or WooCommerce need dynamic product pages, cart sessions, and payment processing.</li>



<li><strong>SaaS dashboards</strong> built in Django or Flask rely on Apache to route requests and manage SSL.</li>



<li><strong>Microservices</strong> written in Node.js often sit behind Apache as a unifying reverse proxy, letting you consolidate multiple backend services under one domain.</li>
</ul>



<h2 class="wp-block-heading">Troubleshooting Common Issues</h2>



<p class="wp-block-paragraph"><strong>500 Internal Server Error</strong> — Check the Apache error log:</p>



<pre class="wp-block-code"><code>sudo tail -f /var/log/apache2/error.log
</code></pre>



<p class="wp-block-paragraph">This almost always points to a permissions issue, a missing module, or a syntax error in your script.</p>



<p class="wp-block-paragraph"><strong>Blank Page with No Error</strong> — For PHP, make sure <code>display_errors</code> is enabled in <code>php.ini</code> while debugging:</p>



<pre class="wp-block-code"><code>display_errors = On
error_reporting = E_ALL
</code></pre>



<p class="wp-block-paragraph"><strong>502 Bad Gateway on Proxy Setups</strong> — This usually means the backend application (Node.js, Python) isn&#8217;t running or isn&#8217;t listening on the port you configured. Double-check with:</p>



<pre class="wp-block-code"><code>sudo netstat -tulnp | grep 3000
</code></pre>



<p class="wp-block-paragraph"><strong>Module Not Loading</strong> — Verify with <code>apache2ctl -M</code> and check that you ran <code>a2enmod</code> followed by a restart, not just a reload.</p>



<h2 class="wp-block-heading">Security Best Practices</h2>



<ul class="wp-block-list">
<li>Disable directory listing with <code>Options -Indexes</code> in your virtual host config.</li>



<li>Keep PHP, Python, and Node runtimes patched and updated.</li>



<li>Use <code>mod_security</code> as a web application firewall layer if you&#8217;re handling sensitive data.</li>



<li>Never expose debug pages like <code>phpinfo()</code> in production.</li>



<li>Run application processes as a non-root user.</li>
</ul>



<h2 class="wp-block-heading">Performance Optimization Tips</h2>



<ul class="wp-block-list">
<li>Use <code>mod_deflate</code> to compress responses.</li>



<li>Enable <code>mod_expires</code> for caching static assets referenced by dynamic pages.</li>



<li>For PHP, switch to PHP-FPM with <code>mod_proxy_fcgi</code> instead of <code>mod_php</code> — it&#8217;s more efficient under concurrent load.</li>



<li>Tune <code>MaxRequestWorkers</code> in your MPM configuration to match your server&#8217;s resources.</li>
</ul>



<h2 class="wp-block-heading">FAQs</h2>



<p class="wp-block-paragraph"><strong>Do I need mod_php for PHP to work?</strong> No — you can also run PHP through PHP-FPM with <code>mod_proxy_fcgi</code>, which is generally faster and more scalable for production.</p>



<p class="wp-block-paragraph"><strong>Can Apache serve multiple dynamic languages at once?</strong> Yes. You can run PHP on one virtual host, proxy to Python on another, and proxy to Node.js on a third — all from the same Apache instance.</p>



<p class="wp-block-paragraph"><strong>Is Apache the best choice for dynamic content compared to Nginx?</strong> Both are capable. Apache&#8217;s <code>.htaccess</code> flexibility and module ecosystem make it appealing for shared hosting and legacy apps, while Nginx is often favored for raw performance in reverse proxy scenarios. The right choice depends on your existing stack and team familiarity.</p>



<h2 class="wp-block-heading">Summary and Key Takeaways</h2>



<p class="wp-block-paragraph">Setting up Apache for dynamic content isn&#8217;t complicated once you understand the moving parts: pick the right module or proxy method for your language, configure your virtual host correctly, and lock down permissions and error handling before going live. Whether you&#8217;re running classic PHP, a Python WSGI app, or proxying to Node.js, Apache gives you a stable, well-documented front end to build on.</p>



<h2 class="wp-block-heading">References</h2>



<ul class="wp-block-list">
<li><a href="https://httpd.apache.org/docs/">Apache HTTP Server Documentation</a></li>



<li><a href="https://modwsgi.readthedocs.io/">Apache mod_wsgi Documentation</a></li>



<li><a href="https://httpd.apache.org/docs/2.4/mod/mod_proxy.html">Apache mod_proxy Documentation</a></li>
</ul>



<p class="wp-block-paragraph"></p>
<p>The post <a href="https://awjunaid.com/apache/how-to-set-up-apache-for-serving-dynamic-content/">How to Set Up Apache for Serving Dynamic Content</a> appeared first on <a href="https://awjunaid.com">Abdul Wahab Junaid</a>.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://awjunaid.com/apache/how-to-set-up-apache-for-serving-dynamic-content/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">5019</post-id>	</item>
	</channel>
</rss>
