Once I started containerizing my applications, I had to rethink how load balancing worked. Backend servers were no longer fixed IPs sitting on physical or virtual machines — they were ephemeral containers that could be recreated, rescheduled, or scaled at any moment. In this post, I’ll walk through how I set up Apache as a load balancer in front of Docker containers, including the networking details that tripped me up the first time around.
Why Combine Apache with Docker for Load Balancing
Docker makes it trivial to spin up multiple identical instances of an application. Apache, running either on the host or in its own container, can sit in front of those instances and distribute traffic across them — giving you the benefits of load balancing without needing a separate hardware appliance or cloud load balancer for smaller deployments.
Prerequisites
- Docker and Docker Compose installed
- Basic familiarity with Dockerfiles and docker-compose.yml
- Apache 2.4+ (I’ll run this in its own container)
- Your application containerized and ready to run multiple instances
Step 1: Set Up Your Application Containers
Here’s a simple example using a Node.js app, but the same principles apply to PHP, Python, or any other backend.
# Dockerfile for app
FROM node:18-alpine
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
EXPOSE 3000
CMD ["node", "app.js"]
Step 2: Create a Docker Compose File with Multiple App Instances
version: "3.8"
services:
app1:
build: ./app
container_name: app1
networks:
- appnet
app2:
build: ./app
container_name: app2
networks:
- appnet
app3:
build: ./app
container_name: app3
networks:
- appnet
apache-lb:
image: httpd:2.4
container_name: apache-lb
ports:
- "80:80"
volumes:
- ./apache/httpd.conf:/usr/local/apache2/conf/httpd.conf
- ./apache/balancer.conf:/usr/local/apache2/conf/extra/balancer.conf
depends_on:
- app1
- app2
- app3
networks:
- appnet
networks:
appnet:
driver: bridge
Notice I’m not exposing ports on app1, app2, and app3 to the host — only Apache needs to reach them, and it does so over the internal appnet Docker network using container names as hostnames.
Step 3: Configure Apache to Reference Container Names
This is the part that’s genuinely different from a traditional setup: instead of IP addresses, you reference Docker container names, which Docker’s internal DNS resolves automatically.
# apache/balancer.conf
<Proxy "balancer://appcluster">
BalancerMember "http://app1:3000"
BalancerMember "http://app2:3000"
BalancerMember "http://app3:3000"
ProxySet lbmethod=byrequests
</Proxy>
<VirtualHost *:80>
ProxyPreserveHost On
ProxyPass "/" "balancer://appcluster/"
ProxyPassReverse "/" "balancer://appcluster/"
</VirtualHost>
Make sure your main httpd.conf loads the necessary modules and includes this file:
LoadModule proxy_module modules/mod_proxy.so
LoadModule proxy_http_module modules/mod_proxy_http.so
LoadModule proxy_balancer_module modules/mod_proxy_balancer.so
LoadModule lbmethod_byrequests_module modules/mod_lbmethod_byrequests.so
Include conf/extra/balancer.conf
Step 4: Start the Stack
docker-compose up -d --build
Verify all containers are running:
docker-compose ps
Test the load balancer:
for i in {1..6}; do curl -s http://localhost/ ; echo; done
If your app returns something identifying which instance responded (like a hostname or container ID), you should see requests rotating across app1, app2, and app3.
Step 5: Scaling Containers Dynamically
One of the advantages of this setup is how easily you can scale. With Docker Compose:
docker-compose up -d --scale app1=3
However, I’ll be honest — Apache’s static BalancerMember configuration doesn’t automatically pick up new containers created this way. For truly dynamic scaling, you have two practical options:
- Manually update
balancer.confand reload Apache whenever you scale (fine for smaller, less dynamic setups). - Use a service discovery / templating tool like
consul-templateor a custom script that regeneratesbalancer.confbased on currently running containers, then triggersapachectl graceful.
For most small-to-medium deployments I’ve worked on, option one combined with a simple deploy script has been sufficient.
Step 6: Reload Apache After Configuration Changes
docker exec apache-lb httpd -k graceful
This applies new balancer members without dropping active connections — important if you’re scaling under live traffic.
Real-World Use Cases
- Local development environments that mimic production load balancing behavior using Docker Compose.
- Small-to-medium production deployments where a full orchestration platform like Kubernetes is overkill, but you still want horizontal scaling and load distribution.
- CI/CD pipelines that spin up ephemeral multi-container environments to test load-balanced behavior before deploying.
Troubleshooting Common Issues
Apache Can’t Resolve Container Names — confirm all services are on the same Docker network (appnet in this example); containers on different networks can’t resolve each other by name.
502 Bad Gateway Immediately After Startup — Apache may have started before the app containers were ready. Add a healthcheck and depends_on with condition: service_healthy:
app1:
build: ./app
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:3000/health"]
interval: 5s
retries: 5
Balancer Config Not Reflecting After Rebuild — if you mounted balancer.conf as a volume, confirm the host file was actually updated; Docker won’t rebuild a bind-mounted config file automatically.
Security Best Practices
- Never expose backend container ports directly to the host unless needed for debugging.
- Use a dedicated Docker network for backend-to-load-balancer traffic, isolated from other services.
- Keep your
httpd:2.4base image updated to pick up security patches. - If running Apache itself in Docker, avoid running the container as root where possible.
Performance Optimization Tips
- Use Docker’s
--restart unless-stoppedpolicy on app containers so they recover automatically from crashes, working alongside Apache’s failover. - Keep container images lean to reduce startup time, which matters when scaling out quickly.
- Monitor container resource usage (
docker stats) alongside Apache’s balancer manager for a complete picture of your cluster’s health.
FAQs
Should I run Apache in a container or on the host when load balancing Docker containers? Either works. Running Apache in its own container keeps your setup fully portable and consistent across environments, which is what I generally prefer for reproducibility.
Does this approach work with Kubernetes instead of Docker Compose? The load balancing concepts are similar, but Kubernetes typically handles this natively through Services and Ingress controllers rather than a manually configured Apache balancer. Apache can still be used as an Ingress controller in some setups, but the configuration approach differs from what’s shown here.
How do I handle SSL/TLS in this setup? Terminate SSL at the Apache container using mod_ssl, then proxy plain HTTP to backend containers over the internal Docker network, which is generally considered safe since that traffic never leaves the host.
Summary and Key Takeaways
Load balancing with Apache and Docker containers follows the same core mod_proxy_balancer principles as a traditional setup, but with container names replacing static IPs and Docker networking handling internal DNS resolution. The main gap to be aware of is that Apache’s balancer configuration is static by default — scaling dynamically requires either manual config updates or a templating/service-discovery layer on top.