I recently published a post which covered how I develop locally using Docker Compose. I’m pleased with how I have everything set up as, among many other features, I use an Nginx container to serve projects on localhost subdomains. With my website, for example, I use the subdomain http://jackwhitworth.localhost. For my host machine, this works out of the box as localhost is bound to Docker by default.

The problem is that the containers aren’t set up to resolve localhost addresses. This means that when I’m working on my website via Docker, an internal server-side request to http://jackwhitworth.localhost/*, would get an error.

This is where Dnsmasq comes in to help. By using Dnsmasq as the DNS server, we can control how requests are resolved between containers.

Dnsmasq is a lightweight, easy to configure, DNS forwarder and DHCP server. It is designed to provide DNS and optionally, DHCP, to a small network. It can serve the names of local machines which are not in the global DNS.

https://hub.docker.com/r/strm/dnsmasq

Using Dnsmasq with Docker Compose

Using Dnsmasq within a Docker Compose stack is pretty simple. The overall process is:

  1. Create the Dnsmasq container within docker-compose.yml
  2. Create a new network within docker-compose.yml
  3. Assign a static IP address to any containers which Dnsmasq resolves to
  4. Configure domain resolutions in dnsmasq.conf
  5. Assign Dnsmasq’s IP as the DNS for services within docker-compose.yml

Add Dnsmasq service & network to Docker Compose file

# docker-compose.yml

version: '3.9'

  # OTHER CONTAINERS

  dnsmasq:
    container_name: dnsmasq
    image: strm/dnsmasq
    volumes:
      - ./dnsmasq.conf:/etc/dnsmasq.conf
    ports:
      - 53:53/udp
    cap_add:
      - NET_ADMIN
    networks:
      default:
        ipv4_address: 172.16.1.1

networks:
  default:
    ipam:
      config:
        - subnet:   172.16.0.0/23
          ip_range: 172.16.0.0/24

We’re creating a new network with fixed IP addresses so that we can then control where traffic is being routed. Within the new dnsmasq service, you’ll notice that we’ve set a static IP address for this new network and a volume has been added to bind a file called dnsmasq.conf to /etc/dnsmasq.conf. We’ll configure the content of this file shortly.

Assign a static IP address to Nginx in Docker Compose

# docker-compose.yml

nginx:
  # Other properties
  networks:
    default:
      # Static IP here so dnsmasq can point to Nginx easily
      ipv4_address: 172.16.1.2

This is a stripped-down version of the nginx service container within my Docker Compose file. I’ve removed a few properties to keep it clear, but you can see the full file here if you’re interested.

I’ve added the IP address 172.16.1.2 to the Nginx container, meaning that the IP will persist between reboots. As this is on the same network as the Dnsmasq container, these two can now interact with each other via this address.

The IP of Nginx needs to be static for the next part to work.

Configure dnsmasq.conf

# dnsmasq.conf

# dnsmasq is used to allow us to avoid adding each new project to our host machine and containers hosts files.
# If you don't know what this does, just leave it be. It allows the PHP containers to ping the Nginx container correctly.

# Use Google's DNS servers
server=8.8.4.4
server=8.8.8.8

# Explicitly define host-ip mappings
address=/localhost/172.16.1.2
# dnsmasq entries are always wildcard entries, so this maps both localhost and *.localhost

This file is what configures the behaviour of Dnsmasq. The comments on this file are pretty comprehensive, but I’ll quickly summarise it:

Two DNS servers are used: 8.8.4.4 and 8.8.8.8. These are Google’s and will resolve any requests for external services on the internet.

The address localhost is then bound to the IP address 172.16.1.2. This is the same IP address we gave Nginx earlier. Because of this, any requests made to localhost while using Dnsmasq as a DNS service will now resolve to our Nginx container.

Within the dnsmasq.conf file, the pattern for binding addresses is always address=/{DOMAIN}/{IP}.

Set Dnsmasq as the DNS service for other containers

php-fpm:
  # Other properties
  dns:
    - 172.16.1.1

The last step is to assign Dnsmasq as the DNS service for the desired containers. Here you can see I’ve added it as the DNS service for a container called php-fpm. From here onwards, after a reboot, any requests for *.localhost from within this container will now resolve to Nginx, which is ready to serve *.localhost domains.

Conclusion

Dnsmasq resolves a subtle, but real problem that becomes apparent when adopting Docker Compose as a LAMP stack. By allowing containers to resolve requests to each other with custom domain names, you can ensure that all containers can communicate effectively, just as traditional bare-metal hardware would in production environments.

If you’d like extra context or resources for what I’ve written up here, I’d encourage you to study the Github repo for my open-source Docker Compose LAMP stack which I use for all of my web development.