How to Configure Apache to Use a Custom Error Page

How to configure Apache to use a custom error page

How to configure Apache to use a custom error page

I still remember the plain, unbranded “404 Not Found” page that greeted visitors on one of my earliest projects — it looked broken even when the rest of the site was working fine. Once I started designing custom error pages, bounce rates on those dead-end URLs noticeably improved, and the site just felt more finished. Here’s exactly how I set custom error pages up in Apache, from a simple static HTML page to something dynamic.

Why Custom Error Pages Matter to Me

Prerequisites

Step 1: Create My Custom Error Pages

I put them in a dedicated directory inside the document root:

sudo mkdir -p /var/www/html/errors

/var/www/html/errors/404.html:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Page Not Found</title>
    <style>
        body { font-family: sans-serif; text-align: center; padding: 80px 20px; }
        h1 { font-size: 72px; margin-bottom: 0; }
        p { font-size: 18px; color: #555; }
        a { color: #0066cc; }
    </style>
</head>
<body>
    <h1>404</h1>
    <p>Sorry, the page you're looking for doesn't exist.</p>
    <p><a href="/">Return to homepage</a></p>
</body>
</html>

I repeat this for other codes I care about: 403.html (Forbidden), 500.html (Internal Server Error), 503.html (Service Unavailable).

Step 2: Configure ErrorDocument Directives

ErrorDocument 400 /errors/400.html
ErrorDocument 401 /errors/401.html
ErrorDocument 403 /errors/403.html
ErrorDocument 404 /errors/404.html
ErrorDocument 500 /errors/500.html
ErrorDocument 502 /errors/502.html
ErrorDocument 503 /errors/503.html

These paths are relative to the web root, so /errors/404.html resolves against DocumentRoot, not the filesystem root — a distinction that trips people up.

Step 3: Apply Inside the Virtual Host

<VirtualHost *:80>
    ServerName example.com
    DocumentRoot /var/www/html

    ErrorDocument 404 /errors/404.html
    ErrorDocument 500 /errors/500.html
    ErrorDocument 403 /errors/403.html

    <Directory /var/www/html>
        AllowOverride All
        Require all granted
    </Directory>
</VirtualHost>

Step 4: Test and Reload

sudo apachectl configtest
sudo systemctl reload apache2

I always test a 404 directly:

curl -I http://example.com/this-page-does-not-exist

I want to see HTTP/1.1 404 Not Found in the headers, while the browser shows my custom page.

Using .htaccess Instead of Virtual Host Config

When I don’t have access to the main config (typical on shared hosting), I drop this into .htaccess inside the document root, provided AllowOverride All (or at least AllowOverride FileInfo) is enabled for that directory:

ErrorDocument 404 /errors/404.html
ErrorDocument 403 /errors/403.html
ErrorDocument 500 /errors/500.html

Using an External URL as an Error Page

ErrorDocument also accepts a full URL, which I’ve used to redirect to a separate error-handling service:

ErrorDocument 404 https://status.example.com/404

One thing I keep in mind: this triggers a redirect, which changes how some clients interpret the status code — search engines can treat this differently than a same-server error page, which can affect SEO signals. For most of my sites, I stick with a local error page.

Dynamic Error Pages (PHP Example)

For more dynamic behavior, like logging the broken URL, I point ErrorDocument at a script:

ErrorDocument 404 /errors/404.php
<?php
// /var/www/html/errors/404.php
http_response_code(404);
$requested = htmlspecialchars($_SERVER['REQUEST_URI'] ?? '', ENT_QUOTES);
error_log("404 hit: " . $requested);
?>
<!DOCTYPE html>
<html>
<head><title>Page Not Found</title></head>
<body>
    <h1>404 - Not Found</h1>
    <p>We couldn't find: <?php echo $requested; ?></p>
    <p><a href="/">Go back home</a></p>
</body>
</html>

I always sanitize any output that echoes the requested URL back, to avoid reflected XSS.

Custom Error Pages for Maintenance (503)

A pattern I use for planned maintenance:

ErrorDocument 503 /errors/maintenance.html
RewriteEngine On
RewriteCond %{REQUEST_URI} !/errors/maintenance.html
RewriteCond %{DOCUMENT_ROOT}/maintenance.flag -f
RewriteRule ^ - [R=503,L]
Header always set Retry-After "3600"

I create maintenance.flag in the document root to trigger the maintenance page for all requests, and delete it to restore normal service.

Real-World Use Cases

Mistakes I’ve Made

Security Best Practices

Performance Optimization

Troubleshooting

Custom error page not showing, default Apache page still appears I verify the ErrorDocument path is web-root-relative and correct, and confirm the config actually got reloaded.

Error page shows but returns status 200 instead of 404 This happens when ErrorDocument points to a redirect/external URL rather than a page served directly by Apache — I switch to a local file path.

.htaccess ErrorDocument directive ignored I check that AllowOverride All or AllowOverride FileInfo is set for that directory in the main config — .htaccess directives are ignored entirely if AllowOverride None is set.

FAQs

Can I use the same custom page for multiple error codes? Yes, I can point several ErrorDocument directives at the same file for a generic experience, though I usually tailor at least the 404 and 500 pages separately.

Will a custom error page hurt SEO? No — as long as the correct HTTP status code still comes back, search engines correctly treat the page as an error page regardless of custom design.

Should error pages be hosted on a CDN? For extra resilience during origin outages, I’ve seen sites host static fallback error pages on a CDN or separate infrastructure so visitors still see something meaningful even if the origin is completely down.

Summary and Key Takeaways

References

Exit mobile version