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.42.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.49f4c2a1b8e3d...# Check it started cleanly$docker logs osztromok-fallbackAH00558: 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 serverFROMhttpd:2.4# Copy a customised httpd.conf (you extracted and edited this)COPYmy-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)COPYwebsite/ /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:LoadModulerewrite_modulemodules/mod_rewrite.so# Allow .htaccess to override settings in your document root# Find the <Directory "/usr/local/apache2/htdocs"> block and change:AllowOverrideNone# default — change to:AllowOverrideAll# Set your server name to suppress the FQDN warningServerNameosztromok.com# Optional: add a virtual host block at the bottom of httpd.conf
<VirtualHost *:80>
ServerNameosztromok.comServerAliaswww.osztromok.comDocumentRoot/usr/local/apache2/htdocsDirectoryIndexindex.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.
Visitorhttps://osztromok.com
↓ HTTPS request hits Cloudflare's edge
Cloudflare NetworkProxies + DDoS protection + CDN
↓ Cloudflare routes down the tunnel (outbound connection your machine initiated)
↓ Forwards to httpd container by name (Docker DNS)
httpd container · port 80Serves files from bind-mounted /htdocs
↓ Reads from
Host filesystemC:\…\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 Trust → Networks → Tunnels
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.4a3f9c2...# 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_HEREb7e1d4...# 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-tunnelINF 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 -tSyntax 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-apache192.168.1.1 - - [16/Jun/2025:10:45:02 +0000] "GET / HTTP/1.1" 200 4521192.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
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.
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.
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".
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".
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.