Install Apache on Ubuntu with a production baseline
Install Apache 2.4, replace the default page, constrain the listener, set a server identity, open only required firewall ports, and prove restart safety.
Produce a clean Apache host that is ready for named virtual hosts without exposing sample content or an accidental default configuration.
- Ubuntu Server 24.04 LTS
- Apache HTTP Server 2.4.x
- Console and recovery access Keep an independent console session and a copy of the current Apache configuration before changing listeners, modules, or access rules.
- DNS and network plan Resolve every production hostname to the intended public address and list the exact management and client networks that require access.
getent ahosts {{domain}} && ip route show - Port ownership Confirm that TCP port 80 is free or already belongs to the intended Apache instance.
sudo ss -lntp 'sport = :80' - Rollback artifact Snapshot the host or create an archive of the active configuration and web roots before the first material change.
OneLiners never runs these steps or stores secrets. Review placeholders, versions, current state, and change-control requirements before using a command.
Full guide
What you will build
- An Ubuntu-supported Apache 2.4 service with recorded package provenance, listener ownership, boot state, configuration map, log evidence, and a root-owned rollback archive.
- A minimal global identity and disclosure baseline plus a deterministic health document instead of Ubuntu's sample landing page.
- A deliberately public HTTP listener protected by explicit firewall policy, syntax-before-reload discipline, named local routing tests, and a clean handoff to virtual hosts and TLS.
- Apache is enabled and active, only the intended process owns TCP/80, complete configuration parses, and the server answers the requested FQDN with the expected health marker.
- ServerTokens and ServerSignature avoid unnecessary version/OS disclosure, ServerName is unambiguous, and sample content is absent.
- The operator can explain every public port, active module/site/config include, firewall rule, document owner, journal event, and rollback action before adding applications.
Architecture
How the parts fit together
Ubuntu packages Apache as the `apache2` systemd service and split configuration into mods-, conf-, and sites-available with enabled symlinks. A small global conf supplies only server identity and disclosure policy. The default document root contains a root-owned static health marker. Apache parses the complete include graph before reload, while UFW admits public HTTP only after the intended listener and upstream network boundary are understood.
- Inventory Ubuntu release, interfaces, routes, all listeners, web packages, firewall policy, failed units, DNS, and console recovery.
- Archive any existing Apache configuration and web roots before the package transaction or file replacement.
- Install from signed Ubuntu repositories and record the packaged Apache version rather than assuming the upstream patch.
- Enable a minimal global identity baseline and replace public sample content with a deterministic root-owned marker.
- Parse the entire configuration and virtual-host map before changing firewall or service state.
- Apply the reviewed HTTP firewall rule, enable/reload Apache, route a named request to loopback, and inspect service logs plus listener state.
Assumptions
- The host is Ubuntu Server 24.04 LTS, uses the approved Ubuntu repositories, and does not already run an undocumented web server, container proxy, or socket activator on TCP/80.
- The server FQDN has an intended DNS purpose even if public DNS is not yet delegated; local `curl --resolve` provides deterministic pre-DNS testing.
- Public HTTP is actually required. A private management UI or internal service should receive a source-restricted rule instead of the public Apache profile.
- Console access and upstream firewall control remain available while service and host-firewall state changes.
- This baseline serves only a static health marker. Application deployment, TLS, virtual-host isolation, dynamic runtimes, headers, logging, and monitoring require later guides.
Key concepts
- ServerName
- Apache's canonical global server identity used when a request or context does not provide a more specific name.
- ServerTokens
- Controls how much server version detail Apache emits in protocol fields such as the Server response header.
- ServerSignature
- Controls generated footer signatures on server-generated pages; it does not remove every fingerprint.
- Enabled configuration
- An Ubuntu available file selected through a symlink under the corresponding *-enabled directory.
- Graceful reload
- A configuration activation that starts new workers with new policy while allowing established requests to finish where possible.
Before you copy
Values used in this guide
{{serverFqdn}}Canonical baseline hostname used in ServerName and deterministic Host routing tests.
Example: web01.example.net{{domain}}Representative DNS name checked by the shared prerequisite before site-specific configuration.
Example: web01.example.netSecurity and production boundaries
- Apache privilege separation still leaves the daemon able to read served content. Keep configuration root-owned and deploy content through a separate controlled account.
- ServerTokens reduces casual disclosure but is not a vulnerability control; patching, module minimization, TLS, application hardening, and monitoring remain required.
- The packaged `Apache` UFW profile normally permits public IPv4 and IPv6 HTTP. Inspect generated policy instead of assuming its scope.
- Do not serve backup archives, source repositories, secret files, private keys, environment files, or writable upload paths from the document root.
- A successful HTTP response does not prove correct virtual-host isolation or HTTPS. This baseline is intentionally a commissioning stage.
Stop before continuing if
- Stop if TCP/80 belongs to another process, existing Apache state is not archived, or a production web root has no verified backup.
- Do not install or reload after repository signature, package, configtest, or virtual-host-map errors.
- Do not open the firewall until the exact listener, address families, public/private intent, and upstream ACL are understood.
- Do not accept the baseline while the response, Server header, listener owner, systemd state, or journal differs from expectation.
verification
Inventory the host before installation
Capture release, addresses, listeners, firewall state, installed web packages, and failed units so the new service cannot silently collide with an existing one.
Why this step matters
A full starting inventory prevents package installation from silently colliding with an existing ingress owner, firewall policy, failed unit, or retained web application.
What to understand
Capture release, interfaces, routes, process-owned TCP listeners, IPv4/IPv6 firewall rules, web packages, systemd failures, DNS, and current HTTP responses.
Run socket inspection with sudo so process ownership is visible; a free IPv4 port does not prove IPv6 or a socket unit is free.
Identify every existing `/etc/apache2`, `/var/www`, `/srv/www`, certificate, log, and deployment owner before treating the host as fresh.
System changes
- No persistent change; reads host, network, package, firewall, service, and web state.
Syntax explained
ip -brief address- Produces a compact address inventory.
ss -lntp- Lists numeric listening TCP sockets and owning processes.
ufw status verbose- Displays active status, defaults, logging, and rules.
systemctl --failed- Lists services already in a failed state.
cat /etc/os-release && ip -brief address && sudo ss -lntp && sudo ufw status verbose && dpkg-query -W 'apache2*' 'nginx*' 2>/dev/null || true; systemctl --failed --no-pagerPRETTY_NAME="Ubuntu 24.04 LTS" Status: active 0 loaded units listed.
Checkpoint: Checkpoint: inventory
Continue whenUbuntu 24.04 and intended addresses are recorded, TCP/80 ownership is known, and no unrelated failed service remains.
Stop whenAn unknown listener, package origin, web root, firewall rule, failed service, or inaccessible console exists.
If this step fails
Port 80 looks free but installation later reports address in use.
Likely causeAn IPv6 listener, socket unit, container, or race was omitted.
sudo ss -lntp '( sport = :80 )'systemctl list-sockets --all
ResolutionFind the owning lifecycle and resolve it deliberately before starting Apache.
Security notes
- Inventory output can reveal private topology and package weaknesses; protect the change record.
Alternatives
- Use authoritative CMDB and configuration-management evidence when it includes the same live socket checks.
Stop conditions
- An unknown listener, package origin, web root, firewall rule, failed service, or inaccessible console exists.
command
Preserve any existing web configuration
Create a root-owned archive only when /etc/apache2 already exists; record its checksum so rollback does not depend on memory.
Why this step matters
A root-owned, checksummed archive preserves configuration and content selection so package or file changes can be reversed without relying on memory.
What to understand
The conditional archive distinguishes a genuinely fresh host from one with Apache state; do not let a missing archive masquerade as success on an existing host.
Store the archive outside served roots with mode 0700 on its parent, record checksum, size, included paths, and restore command.
A file archive is not application data consistency; dynamic sites need their own database/storage backup.
System changes
- Creates `/root/oneliners-backups` and, when prior state exists, a compressed archive plus checksum evidence.
Syntax explained
install -d -m 0700- Creates a root-only backup directory.
tar -C / -czf- Archives paths relative to filesystem root using gzip compression.
sha256sum- Records an integrity digest for later restore verification.
sudo install -d -m 0700 /root/oneliners-backups && if test -d /etc/apache2; then sudo tar -C / -czf /root/oneliners-backups/apache-before-install.tgz etc/apache2 var/www; fi && sudo sha256sum /root/oneliners-backups/apache-before-install.tgz 2>/dev/null || echo 'Fresh install: no prior Apache archive'Fresh install: no prior Apache archive
Checkpoint: Checkpoint: backup
Continue whenExisting state produces a non-empty root-only archive whose checksum verifies; a fresh host is explicitly recorded.
Stop whenExisting files are present but archive/checksum fails, storage is insufficient, or application data lies outside scope.
If this step fails
tar reports a file changed while reading.
Likely causeA deployment or service is modifying content during capture.
ps -ef | grep -E '[a]pache|[r]sync|[t]ar'
ResolutionQuiesce the owning deployment or use an application-consistent snapshot, then create a new archive.
Security notes
- The archive may contain credentials and private configuration; never place it under `/var/www`.
Alternatives
- Use a provider/filesystem snapshot with verified restore when it captures all required paths consistently.
Stop conditions
- Existing files are present but archive/checksum fails, storage is insufficient, or application data lies outside scope.
command
Install Apache from Ubuntu repositories
Refresh signed Ubuntu metadata, install the distribution package, and record the exact package version rather than assuming the upstream patch level.
Why this step matters
Installing the signed Ubuntu package gives Apache a maintained service unit, helper tooling, module layout, log integration, and security-update path.
What to understand
Inspect apt-cache policy before installation to detect a PPA or third-party candidate.
Record both `apache2ctl -v` and dpkg version because Ubuntu may backport fixes without changing the upstream-looking version as expected.
Treat signature, dependency, disk, and maintainer-script failures as blockers; do not curl an alternate installer.
System changes
- Installs Apache packages, configuration tree, service unit, default site, web root, logs, runtime account, and may start TCP/80.
Syntax explained
apt update- Downloads and verifies repository indexes.
apt install apache2- Installs Ubuntu's Apache package and dependencies.
--yes- Accepts the reviewed transaction noninteractively.
dpkg-query -W- Records the installed package revision.
sudo apt update && sudo apt install --yes apache2 && apache2ctl -v && dpkg-query -W apache2Server version: Apache/2.4.x (Ubuntu) apache2 2.4.x-ubuntu
Checkpoint: Checkpoint: install
Continue whenPackage origin and version are approved, installation exits zero, and any automatically started listener is Apache on intended interfaces.
Stop whenRepository trust, package plan, maintainer script, listener ownership, or version is unexpected.
If this step fails
Package config fails because the service cannot bind port 80.
Likely causeAn existing process or socket acquired the port.
sudo ss -lntp 'sport = :80'sudo journalctl -u apache2 --since '10 minutes ago' --no-pager
ResolutionKeep the firewall closed, identify the owner, and restart package configuration only after resolving ingress ownership.
Security notes
- Package installation can enable a default public listener; verify firewall and upstream ACL immediately.
Alternatives
- Stage installation in an image or configuration-management pipeline with the same signed packages.
Stop conditions
- Repository trust, package plan, maintainer script, listener ownership, or version is unexpected.
config
Set an explicit global ServerName
Use the server FQDN to suppress ambiguous identity warnings while keeping site-specific names inside virtual hosts.
Why this step matters
A minimal global identity removes ambiguous startup behavior and reduces generated disclosure while keeping domain routing inside virtual hosts.
What to understand
ServerName should be a stable administrative FQDN, not a user-controlled Host value or temporary address.
ServerTokens Prod reduces header detail and ServerSignature Off removes generated-page footers; neither replaces patching.
Use a conf-available file and a2enconf so selection is explicit and rollback removes one symlink/file.
System changes
- Creates and enables `/etc/apache2/conf-available/10-oneliners-baseline.conf` with three global directives.
Syntax explained
ServerName- Defines Apache's canonical global server identity.
ServerTokens Prod- Limits protocol banner detail to the product name.
ServerSignature Off- Suppresses Apache footer signatures on generated pages.
a2enconf- Creates the Ubuntu conf-enabled selection symlink.
/etc/apache2/conf-available/10-oneliners-baseline.confValues stay on this page and are never sent or saved.
printf 'ServerName {{serverFqdn}}\nServerTokens Prod\nServerSignature Off\n' | sudo tee /etc/apache2/conf-available/10-oneliners-baseline.conf && sudo a2enconf 10-oneliners-baselineEnabling conf 10-oneliners-baseline.
Checkpoint: Checkpoint: identity
Continue whenThe conf is root-owned, enabled exactly once, parser accepts it, and runtime dump shows the intended ServerName.
Stop whenFQDN is unapproved, the file is writable by deployment users, duplicate directives conflict, or parser warns.
If this step fails
The FQDN warning remains.
Likely causeThe conf is not enabled, a syntax error prevents loading, or another context overrides identity.
sudo a2query -c 10-oneliners-baselinesudo apache2ctl -t -D DUMP_RUN_CFG
ResolutionFix the enabled include and rerun complete configtest before reload.
Security notes
- Do not publish internal naming that violates the intended external identity policy.
Alternatives
- Use an IP-safe placeholder only on isolated test hosts while a stable name is assigned.
Stop conditions
- FQDN is unapproved, the file is writable by deployment users, duplicate directives conflict, or parser warns.
config
Replace the distribution landing page
Create a minimal health document owned by root and served read-only; remove public sample information without deleting the web root.
Why this step matters
Replacing sample content prevents public disclosure of installation details and gives routing checks a deterministic marker owned outside the web worker.
What to understand
The health file is static, contains no secret/runtime data, and is root-owned mode 0644 so Apache reads but cannot modify it.
Keep the document root directory non-writable by `www-data`; application uploads belong outside executable/static roots with explicit handling.
This temporary default is replaced by an explicit rejecting host when named sites are introduced.
System changes
- Replaces `/var/www/html/index.html` content and enforces root ownership plus read-only public mode.
Syntax explained
tee ... >/dev/null- Writes exact deterministic content while suppressing duplicate stdout.
chown root:root- Prevents the Apache runtime account from modifying served content.
chmod 0644- Allows public read but only root write.
/var/www/html/index.htmlprintf '<!doctype html><title>Apache ready</title><h1>Apache ready</h1>\n' | sudo tee /var/www/html/index.html >/dev/null && sudo chown root:root /var/www/html/index.html && sudo chmod 0644 /var/www/html/index.html-rw-r--r-- 1 root root /var/www/html/index.html
Checkpoint: Checkpoint: default site
Continue whenThe exact marker exists, is root-owned 0644, and no sample or secret files are reachable in the root.
Stop whenThe path is a symlink/unexpected mount, retained application content would be overwritten, or runtime user can write it.
If this step fails
The file changes but HTTP still shows old content.
Likely causeAnother vhost/root, cache, proxy, or address receives the request.
sudo apache2ctl -Scurl --resolve 'web01.example.net:80:127.0.0.1' http://web01.example.net/
ResolutionIdentify the selected vhost and effective DocumentRoot before changing any other file.
Security notes
- Never use phpinfo, environment dumps, or server-status as a public health document.
Alternatives
- Serve a fixed 204 response from an explicit health location when no document root is desired.
Stop conditions
- The path is a symlink/unexpected mount, retained application content would be overwritten, or runtime user can write it.
verification
Validate the complete configuration
Run Apache's parser and virtual-host dump before reloading; do not treat an active systemd unit as proof that the new files are valid.
Why this step matters
Parsing the full include graph and dumping host selection catches errors and accidental defaults before service or firewall activation.
What to understand
configtest validates syntax but not file readability at request time, application correctness, or external DNS.
`apache2ctl -S` shows default vhosts, name/alias mapping, ports, and source file lines; review every entry.
Also inspect loaded modules and runtime config when the host was not freshly installed.
System changes
- No persistent change; parses Apache configuration and displays the virtual-host map.
Syntax explained
configtest- Parses the complete effective Apache configuration.
-S- Shows virtual-host selection, aliases, defaults, and source files.
sudo apache2ctl configtest && sudo apache2ctl -SSyntax OK *:80 is a NameVirtualHost
Checkpoint: Checkpoint: syntax
Continue whenSyntax OK appears and the only TCP/80 default/site is the intentional baseline with known source files.
Stop whenAny warning/error, duplicate host, unknown include/module, or unintended default appears.
If this step fails
Syntax is OK but -S shows duplicate defaults.
Likely causeMultiple enabled site symlinks define the same listener/name.
find /etc/apache2/sites-enabled -maxdepth 1 -type l -ls
ResolutionDisable only the duplicate enabled selection after confirming file ownership and intended default.
Security notes
- Treat unknown enabled modules and includes as executable policy, not harmless clutter.
Alternatives
- Run `apache2ctl -t -D DUMP_RUN_CFG -D DUMP_VHOSTS -D DUMP_MODULES` for deeper review.
Stop conditions
- Any warning/error, duplicate host, unknown include/module, or unintended default appears.
warning
Open HTTP only after confirming the listener
Preview the firewall change, allow the packaged Apache profile, and confirm no unrelated public ports were added.
Why this step matters
Previewing and then enabling only the packaged HTTP profile makes public exposure an explicit change after listener and configuration ownership are proven.
What to understand
Inspect the profile definition and generated IPv4/IPv6 rules. Public HTTP is appropriate only for a public web baseline.
Compare host policy with cloud firewall, load balancer, NAT, and routing; a permissive upstream layer or alternate IPv6 path can change exposure.
Test from a real external network after activation and keep the console open.
System changes
- Adds persistent UFW allow rules for the Apache HTTP profile, normally public TCP/80 for enabled address families.
Syntax explained
--dry-run- Prints generated UFW policy without applying it.
allow 'Apache'- Applies the packaged Apache HTTP application profile.
status numbered- Shows active rule order and deletion identifiers.
sudo ufw --dry-run allow 'Apache' && sudo ufw allow 'Apache' && sudo ufw status numbered[ 1] Apache ALLOW IN Anywhere
Checkpoint: Checkpoint: firewall
Continue whenOnly intended TCP/80 rules are added and no unrelated public port becomes reachable.
Stop whenThe service is private, profile includes unexpected ports, upstream policy is unknown, or console recovery is absent.
If this step fails
Remote HTTP remains unreachable.
Likely causeUpstream ACL, routing, NAT, IPv6, listener binding, or UFW order blocks traffic.
sudo ufw status numberedsudo tcpdump -ni any tcp port 80 -c 20
ResolutionTrace the intended flow and correct the smallest layer; never open arbitrary ports.
Security notes
- Opening HTTP allows any reachable client to exercise Apache and served applications; deploy monitoring and patching before content.
Alternatives
- Use a source-restricted `ufw allow from CIDR to any port 80 proto tcp` for private services.
Stop conditions
- The service is private, profile includes unexpected ports, upstream policy is unknown, or console recovery is absent.
verification
Enable, reload, and test the service locally
Enable boot start, reload the validated configuration, request the health document by name, and inspect the latest service messages.
Why this step matters
Enabling boot state, gracefully loading validated configuration, and sending a named local request prove the actual service path rather than only static files.
What to understand
`enable --now` changes both future boot behavior and current service state; reload then applies the latest validated configuration.
`curl --resolve` routes the requested FQDN to loopback while preserving the Host header, isolating Apache from DNS propagation.
Inspect journal, response headers/body, listener owner, and failed units after activation; a 200 alone is incomplete evidence.
System changes
- Enables and starts apache2, reloads configuration, creates request/access-log evidence, and preserves boot activation.
Syntax explained
systemctl enable --now- Enables boot start and starts the service immediately.
systemctl reload- Gracefully applies already validated configuration.
curl --fail- Returns failure for HTTP error status.
--resolve host:80:127.0.0.1- Overrides DNS while retaining the intended Host header.
Values stay on this page and are never sent or saved.
sudo systemctl enable --now apache2 && sudo systemctl reload apache2 && curl --fail --silent --show-error --resolve '{{serverFqdn}}:80:127.0.0.1' http://{{serverFqdn}}/ && sudo journalctl -u apache2 -n 20 --no-pager<h1>Apache ready</h1> Started apache2.service - The Apache HTTP Server.
Checkpoint: Checkpoint: activate
Continue whenService is enabled/active, named request returns only the marker, logs show a clean reload, and Apache alone owns TCP/80.
Stop whenStart/reload fails, response routes elsewhere, header exposes unexpected detail, journal errors, or listener ownership differs.
If this step fails
Reload succeeds but the old response remains.
Likely causeThe request targets another vhost/cache or file path, not a stale Apache process.
sudo apache2ctl -Scurl -v --resolve 'web01.example.net:80:127.0.0.1' http://web01.example.net/
ResolutionTrace vhost selection and cache headers before restarting or deleting content.
Security notes
- Do not leave the temporary default as the final multi-tenant fallback; introduce a rejecting default before named sites.
Alternatives
- Use `apache2ctl graceful` through the managed service when systemd reload is unavailable and policy permits.
Stop conditions
- Start/reload fails, response routes elsewhere, header exposes unexpected detail, journal errors, or listener ownership differs.
Finish line
Verification checklist
sudo apache2ctl configtest && systemctl is-enabled apache2 && systemctl is-active apache2Syntax OK, enabled, and active.curl --fail --head --resolve '{{serverFqdn}}:80:127.0.0.1' http://{{serverFqdn}}/Returns HTTP/1.1 200 OK with ServerTokens not exposing the OS version.sudo ss -lntp 'sport = :80'Only Apache owns the intended port 80 listeners.Recovery guidance
Common problems and safe checks
APT cannot install Apache because another package or process owns TCP/80.
Likely causenginx, a container publisher, a previous Apache build, or socket activation already provides HTTP.
sudo ss -lntp 'sport = :80'systemctl list-sockets --alldocker ps --format '{{.Names}} {{.Ports}}'
ResolutionIdentify the intended ingress owner and migrate or stop it under a separate rollback plan; never kill an unknown listener.
apache2ctl configtest reports `Could not reliably determine the server's fully qualified domain name`.
Likely causeNo valid global ServerName is enabled, the conf symlink is absent, or the configured name is malformed.
sudo a2query -c 10-oneliners-baselinesudo apache2ctl -t -D DUMP_RUN_CFG
ResolutionSet one stable FQDN in the root-owned baseline conf, enable it, and repeat parser and runtime dumps.
Local curl succeeds but a remote client times out.
Likely causeUFW, provider ACL, routing, address family, NAT, or listener binding blocks the public path.
sudo ss -lntp 'sport = :80'sudo ufw status numberedsudo tcpdump -ni any tcp port 80 -c 20
ResolutionTrace each boundary and correct the smallest intended rule; do not add unrelated broad ports.
Apache serves the Ubuntu sample page instead of the health marker.
Likely causeThe request reached another vhost/root, a proxy/cache, or the file replacement targeted a different path.
sudo apache2ctl -Scurl --resolve 'web01.example.net:80:127.0.0.1' http://web01.example.net/readlink -f /var/www/html/index.html
ResolutionUse the vhost map and named local request to identify the active DocumentRoot, then replace only the intended file.
Reference
Frequently asked questions
Should port 80 remain open after HTTPS?
Usually yes for HTTP-to-HTTPS redirects and ACME HTTP-01, but it should serve no sensitive application content and must be monitored.
Does ServerTokens Prod hide Apache?
It reduces banner detail but software can still be fingerprinted. Keep Apache patched and minimize modules.
Why test with curl --resolve?
It sets both destination address and HTTP Host deterministically without waiting for DNS, so routing can be proven before public cutover.
Recovery
Rollback
Disable the new service and firewall rule, then restore the archived configuration if this replaced an earlier deployment.
- Remove the UFW Apache rule by its numbered entry and verify port 80 is no longer public.
- Disable and stop apache2 before restoring files from /root/oneliners-backups/apache-before-install.tgz.
- Run apache2ctl configtest after restoration and start only the intended previous service.
- Purge apache2 only for a confirmed fresh install; never purge when /var/www contains retained application data.
Evidence