Scenario: Apache Web Server

Chapter 7 — Scenario: Apache Web Server

Time to put Docker's fundamentals to practical use. In this chapter you'll build a working Apache web server inside a Docker container that serves your actual website files — and then make it publicly accessible on the internet using Cloudflare Tunnel, with no router configuration, no port forwarding, and no static IP required.

This gives you a useful fallback for osztromok.com: if your primary server ever goes down for maintenance or has a problem, you can spin up this container on any machine — your Raspberry Pi, a laptop, a VPS — and the site stays up while you fix the real issue. The whole thing takes about ten minutes once it's configured.

What we're building

🌐
Visitorosztromok.com
☁️
CloudflareDNS + Proxy
🔒
CF TunnelEncrypted outbound
🐳
cloudflaredcontainer
🖥️
httpdcontainer :80
📁
Your site filesbind mount

We'll run two containers on a shared Docker network: one running Apache (httpd) serving your site files, and one running cloudflared creating a secure outbound tunnel to Cloudflare's network. Visitors reach your site through Cloudflare — your machine never needs an open inbound port.

1
The Official httpd Image

The httpd image is the official Apache HTTP Server image on Docker Hub, maintained by the Apache project. It's production-ready, security- patched regularly, and remarkably easy to use — just mount your files at the right path and it serves them.

Terminal — pull and inspect
# Pull the latest stable Apache image $ docker pull httpd:2.4 2.4: Pulling from library/httpd Digest: sha256:a5f58c... Status: Downloaded newer image for httpd:2.4 # Where does httpd serve files from inside the container? $ docker inspect httpd:2.4 --format '{{json .Config.Env}}' ["PATH=/usr/local/apache2/bin:...", "HTTPD_VERSION=2.4.62"] # The document root is /usr/local/apache2/htdocs # Quick test — serve the default Apache page $ docker run -d -p 8080:80 --name apache-test httpd:2.4 $ curl http://localhost:8080 <html><body><h1>It works!</h1></body></html> $ docker rm -f apache-test
Why tag 2.4 not latest? The latest tag follows whatever the most recent release is. For a web server used as a fallback, you want predictability — pin to 2.4 and you'll always get the current 2.4.x patch release, never accidentally jump to a major version that might change behaviour.
2
Mounting Your Site Files

Apache inside the container looks for files in /usr/local/apache2/htdocs. We'll use a bind mount to point that path at your actual website folder on the host machine. Any changes you make to the files on disk are immediately visible to Apache — no restart, no rebuild.

# Replace the path with your actual website folder # Windows PowerShell — use ${PWD} or an absolute path docker run -d \ --name osztromok-fallback \ -p 8080:80 \ -v C:\Users\emuba\OneDrive\My Learning\WebSites\website:/usr/local/apache2/htdocs:ro \ httpd:2.4 # :ro makes it read-only — Apache should never write to your source files
Terminal — verify the site loads
$ docker run -d --name osztromok-fallback -p 8080:80 -v "C:\Users\emuba\OneDrive\My Learning\WebSites\website":/usr/local/apache2/htdocs:ro httpd:2.4 9f4c2a1b8e3d... # Check it started cleanly $ docker logs osztromok-fallback AH00558: httpd: Could not reliably determine the server's fully qualified domain name [Mon Jun 16 10:23:01.000000 2025] [mpm_event:notice] [pid 1:tid 1] AH00489: Apache/2.4.62 configured [Mon Jun 16 10:23:01.000001 2025] [mpm_event:notice] [pid 1:tid 1] AH00094: Command line: 'httpd -D FOREGROUND' # The "fully qualified domain name" warning is harmless — ignore it # Open http://localhost:8080 in your browser to see your site
Windows path with spaces: When your path contains spaces (as yours does — "My Learning"), always wrap the entire -v argument in double quotes in PowerShell: -v "C:\Users\emuba\OneDrive\My Learning\WebSites\website":/usr/local/apache2/htdocs:ro
3
Custom Apache Configuration (Optional)

The default httpd.conf works for basic file serving, but you might want to tweak it — for example to enable .htaccess files, set up a virtual host for your domain, or enable mod_rewrite. There are two ways to do this.

Option A — Mount a custom httpd.conf

Extract the default config, edit it, then mount it back in:

Terminal — extract default config
# Copy the default config out of the image so you have something to edit $ docker run --rm httpd:2.4 cat /usr/local/apache2/conf/httpd.conf > my-httpd.conf # Edit my-httpd.conf, then mount it back in $ docker run -d \ --name osztromok-fallback \ -p 8080:80 \ -v "C:\Users\emuba\OneDrive\My Learning\WebSites\website":/usr/local/apache2/htdocs:ro \ -v "$(pwd)/my-httpd.conf":/usr/local/apache2/conf/httpd.conf:ro \ httpd:2.4

Option B — Build a custom image (recommended for repeatability)

If your config changes are permanent, bake them into a custom image. This is the Dockerfile approach from Chapter 6 applied to Apache:

# Dockerfile for a pre-configured Apache fallback server FROM httpd:2.4 # Copy a customised httpd.conf (you extracted and edited this) COPY my-httpd.conf /usr/local/apache2/conf/httpd.conf # Copy your site files directly into the image # (alternative to bind mount — good for a truly portable image) COPY website/ /usr/local/apache2/htdocs/

Key httpd.conf tweaks for osztromok.com

# Enable mod_rewrite (needed for clean URLs / .htaccess rewrites) # Uncomment this line in httpd.conf: LoadModule rewrite_module modules/mod_rewrite.so # Allow .htaccess to override settings in your document root # Find the <Directory "/usr/local/apache2/htdocs"> block and change: AllowOverride None # default — change to: AllowOverride All # Set your server name to suppress the FQDN warning ServerName osztromok.com # Optional: add a virtual host block at the bottom of httpd.conf <VirtualHost *:80> ServerName osztromok.com ServerAlias www.osztromok.com DocumentRoot /usr/local/apache2/htdocs DirectoryIndex index.html index.php </VirtualHost>
Test your config before restarting: Run docker exec osztromok-fallback httpd -t to validate the configuration syntax. Apache prints "Syntax OK" if everything is fine, or a helpful error message if something is wrong.
4
Making It Public with Cloudflare Tunnel

Your container currently answers on localhost:8080 — only accessible from the machine it's running on. To make it reachable at osztromok.com from the internet, you'd normally need to open inbound ports on your router and have a static IP. Cloudflare Tunnel eliminates both requirements entirely.

The cloudflared process runs as its own container, opens an outbound encrypted connection to Cloudflare's edge, and tells Cloudflare to route traffic for your domain down that tunnel to your Apache container — all without a single inbound port.

Visitor https://osztromok.com
↓ HTTPS request hits Cloudflare's edge
Cloudflare Network Proxies + DDoS protection + CDN
↓ Cloudflare routes down the tunnel (outbound connection your machine initiated)
cloudflared container Maintains encrypted tunnel · Docker custom network
↓ Forwards to httpd container by name (Docker DNS)
httpd container · port 80 Serves files from bind-mounted /htdocs
↓ Reads from
Host filesystem C:\…\website\ (bind mount :ro)

Prerequisites

  • Your domain (osztromok.com) must be on Cloudflare's nameservers (it already is if you're using Cloudflare Tunnel for your main server)
  • You need a Cloudflare account with access to the Zero Trust dashboard
  • The tunnel token you'll generate in the next step

Creating the tunnel token

One-time setup in the Cloudflare dashboard:
1. Go to Cloudflare Zero TrustNetworksTunnels
2. Click Add a tunnel → choose Cloudflared
3. Give it a name (e.g. osztromok-docker-fallback)
4. Copy the token — a long string starting with eyJ...
5. Under Public Hostnames, add: Subdomain @, Domain osztromok.com, Service http://osztromok-apache:80

The service URL uses the Docker container name — this is Docker's internal DNS resolving the httpd container by name on the shared network.
5
Running Both Containers Together

From Chapter 5 you know that containers on the same custom network can reach each other by name. We'll create a network, start Apache on it (named osztromok-apache so Cloudflare can route to it), then start cloudflared on the same network.

Terminal — full setup
# 1. Create a dedicated network for these two containers $ docker network create osztromok-net # 2. Start Apache — note the name matches what Cloudflare tunnel points to $ docker run -d \ --name osztromok-apache \ --network osztromok-net \ -v "C:\Users\emuba\OneDrive\My Learning\WebSites\website":/usr/local/apache2/htdocs:ro \ httpd:2.4 a3f9c2... # 3. Start cloudflared — paste your tunnel token in place of TOKEN_HERE $ docker run -d \ --name osztromok-tunnel \ --network osztromok-net \ cloudflare/cloudflared:latest tunnel \ --no-autoupdate run \ --token TOKEN_HERE b7e1d4... # 4. Check both are running $ docker ps --format "table {{.Names}}\t{{.Status}}\t{{.Image}}" NAMES STATUS IMAGE osztromok-tunnel Up 8 seconds cloudflare/cloudflared:latest osztromok-apache Up 12 seconds httpd:2.4 # 5. Watch the tunnel connect $ docker logs osztromok-tunnel INF Starting metrics server on 127.0.0.1:2000/metrics INF Registered tunnel connection connIndex=0 ip=198.41.200.33 location=LHR INF Registered tunnel connection connIndex=1 ip=198.41.200.34 location=LHR # Once you see "Registered tunnel connection" it's live — visit osztromok.com
Don't expose port 8080 when using the tunnel. The tunnel gives Cloudflare access to your container by name on the internal network. You don't need (and shouldn't have) -p 8080:80 on the Apache container in production — that would open it to everyone on your local network. Only add -p when you want localhost access for testing.
6
Saving It as a Startup Script

When your primary server goes down, you want to activate the fallback in seconds, not spend time reconstructing commands. Save this as a PowerShell script and keep it somewhere you can find it quickly.

# start-fallback.ps1 # ────────────────────────────────────────────────────────────────── # osztromok.com Docker fallback web server # Run this if the primary server is down. # ────────────────────────────────────────────────────────────────── $SITE_PATH = "C:\Users\emuba\OneDrive\My Learning\WebSites\website" $CF_TOKEN = "eyJ..." # paste your Cloudflare tunnel token here $NETWORK = "osztromok-net" $APACHE_IMG = "httpd:2.4" $CF_IMG = "cloudflare/cloudflared:latest" # Ensure the images are up to date docker pull $APACHE_IMG docker pull $CF_IMG # Remove any leftover containers from a previous run docker rm -f osztromok-apache osztromok-tunnel 2>&$null # Create network (safe to run even if it already exists) docker network create $NETWORK 2>&$null # Start Apache docker run -d ` --name osztromok-apache ` --network $NETWORK ` --restart unless-stopped ` -v "${SITE_PATH}":/usr/local/apache2/htdocs:ro ` $APACHE_IMG # Start Cloudflare Tunnel docker run -d ` --name osztromok-tunnel ` --network $NETWORK ` --restart unless-stopped ` $CF_IMG tunnel --no-autoupdate run --token $CF_TOKEN Write-Host "Fallback started. Waiting for tunnel to connect..." Start-Sleep 5 docker logs --tail 5 osztromok-tunnel
--restart unless-stopped means both containers will come back up automatically if your machine reboots while the fallback is active. To stop the fallback intentionally, run docker stop osztromok-apache osztromok-tunnel — after an explicit stop, the containers won't auto-restart.

Stopping the fallback when your main server is back

# stop-fallback.ps1 docker stop osztromok-apache osztromok-tunnel docker rm osztromok-apache osztromok-tunnel docker network rm osztromok-net Write-Host "Fallback stopped. Remember to update DNS back to your primary server."
7
Troubleshooting
Symptom
Site shows "403 Forbidden"
Fix
Apache can't read the files. Check that index.html exists in your website root. On Windows you may need to share the drive in Docker Desktop Settings → Resources → File Sharing.
Symptom
Tunnel logs show "failed to connect" or exits immediately
Fix
Wrong or expired token. Re-generate the token in the Cloudflare Zero Trust dashboard, delete the container, and re-run with the new token.
Symptom
tunnel logs show "connection refused" reaching osztromok-apache
Fix
Container names must match exactly. The tunnel config says http://osztromok-apache:80 — verify the Apache container has exactly that name: docker ps --format '{{.Names}}'
Symptom
CSS/images load on homepage but 404 on other pages
Fix
mod_rewrite not enabled. Edit httpd.conf to uncomment the rewrite_module line and set AllowOverride All, then restart: docker restart osztromok-apache
Symptom
Docker Desktop says drive sharing is blocked
Fix
In Docker Desktop → Settings → Resources → File Sharing, add C:\Users\emuba\OneDrive. OneDrive paths sometimes need explicit permission.
Terminal — useful diagnostic commands
# Check Apache is serving files (from inside the container) $ docker exec osztromok-apache ls /usr/local/apache2/htdocs # Validate Apache config syntax $ docker exec osztromok-apache httpd -t Syntax OK # Test Apache is reachable from the cloudflared container $ docker exec osztromok-tunnel wget -qO- http://osztromok-apache:80 # Follow live access logs $ docker logs -f osztromok-apache 192.168.1.1 - - [16/Jun/2025:10:45:02 +0000] "GET / HTTP/1.1" 200 4521 192.168.1.1 - - [16/Jun/2025:10:45:02 +0000] "GET /css/style.css HTTP/1.1" 200 8103
Fallback activation checklist
  • Docker Desktop is running
  • Run start-fallback.ps1
  • Wait for "Registered tunnel connection" in cloudflared logs
  • Visit https://osztromok.com and confirm the site loads
  • Check Cloudflare Zero Trust dashboard → Tunnels → your tunnel is "Healthy"
  • When primary server is back: run stop-fallback.ps1
  • Verify primary server is responding before stopping fallback
Exercises
  1. Local test run. Start just the Apache container (no tunnel yet) with -p 8080:80 and bind-mount your website folder. Visit http://localhost:8080 and navigate around the site. Make a visible edit to index.html (e.g. change a heading), save the file, and refresh the browser — the change should appear instantly without restarting the container, because it reads directly from the bind mount.
  2. Inspect the access log. While the container is running and you browse the site, run docker logs -f osztromok-apache. Notice how each page visit, stylesheet, image and script generates a separate log line. Try visiting a page that doesn't exist and observe the 404 log entry.
  3. Validate your httpd.conf. Extract the default config with docker run --rm httpd:2.4 cat /usr/local/apache2/conf/httpd.conf > my-httpd.conf. Open the file and find the lines that need uncommenting to enable mod_rewrite and set AllowOverride All. Make those changes, mount the config, restart, and run docker exec osztromok-apache httpd -t to confirm "Syntax OK".
  4. Set up a tunnel (if you have Cloudflare access). Follow Step 4 to create a tunnel in Cloudflare Zero Trust. Run both containers using the commands in Step 5, wait for the tunnel to show "Registered", and visit https://osztromok.com — you should see your site served from the Docker container. Check the Cloudflare dashboard to confirm the tunnel is "Healthy".
  5. Create the startup script. Copy the start-fallback.ps1 from Step 6 into a real file on your machine, fill in your Cloudflare token and site path, and do a full test run: stop any running fallback containers, run the script, confirm the site loads, then run stop-fallback.ps1. Store both scripts somewhere you'll find them under pressure — desktop, OneDrive, or a dedicated ops folder.
Next: Chapter 8 — Scenario: Python Development Environment
Now that you've seen Docker used as an operations tool, Chapter 8 switches to the developer angle: building a Python development container for FastAPI work. You'll keep dependencies isolated from your Windows system, use VS Code's Dev Containers extension to edit code inside the container, and never touch your host Python installation again.