Establishing Secure Inter-Service Networking

In a multi-service application, the way your components communicate is as critical as what they do. This chapter focuses on establishing secure and isolated networking for our Docker Compose stack. We’ll move beyond Docker’s default networking to create a dedicated network for our services, enhancing both security and clarity.

By the end of this milestone, our web application and database will communicate over a private, isolated network managed by Docker Compose. This ensures that only authorized services within our stack can reach each other, laying a robust foundation for a production-ready deployment.

Project Overview

Our overarching project goal is to build a production-ready multi-service web application stack using Docker and Docker Compose. This involves containerizing a simple web application, integrating it with a database, and applying best practices for deployment, security, and maintainability.

In this chapter, we specifically address the crucial aspect of inter-service communication. We are moving from implicit, default networking to an explicitly defined, isolated network to secure the communication pathways between our web and db services. This is a fundamental security and architectural pattern for any real-world containerized application.

Tech Stack

For this chapter, we continue to leverage:

  • Docker Engine: The core containerization platform. (Version unknown, checked 2026-05-22. We recommend using the latest stable release available for your OS).
  • Docker Compose: Used to define and run our multi-container Docker application. We adhere to the Compose Specification, which recommends omitting the explicit version field in docker-compose.yml files (checked 2026-05-22).
  • YAML: The language for defining our docker-compose.yml file.

Milestones for Secure Networking

To achieve our goal of isolated inter-service communication, we will follow these steps:

  1. Understand Docker Networking Defaults: Briefly review how Docker Compose handles networking by default.
  2. Define a Custom Bridge Network: Add a networks section to docker-compose.yml to create a dedicated network.
  3. Attach Services to the Network: Explicitly connect our web and db services to this new custom network.
  4. Update Service Discovery: Configure the web service to use the database service name for internal resolution.
  5. Verify Network Configuration: Confirm that the network is correctly created and services are attached and communicating.

Planning & Design: Isolated Service Communication

When you run multiple Docker containers that need to communicate, Docker provides robust networking capabilities. By default, Docker Compose places all services in a single, default network. While functional for simple setups, this default network can become less manageable as your application grows, and it doesn’t explicitly segment traffic.

For production systems, it’s a best practice to define custom networks. This allows you to:

  • Isolate Services: Only services explicitly attached to a network can communicate over it. This prevents unintended communication paths and reduces the attack surface.
  • Improve Name Resolution: Docker’s internal DNS allows services to resolve each other by their service names (e.g., web can reach db by simply using db as the hostname). This is more reliable than relying on dynamic IP addresses.
  • Enhance Security: By default, custom bridge networks are isolated from the host’s network unless specific ports are published. This minimizes external exposure.

Our goal is to define a custom bridge network within our docker-compose.yml file and explicitly attach our web and db services to it.

Network Architecture Overview

Our application’s communication flow will now look like this, with a dedicated, isolated network layer:

flowchart TD User --> Host_Port[Host Port 80] Host_Port --> WebServer[Web Server Container] subgraph App_Network["Application Network"] WebServer --> Database[Database Container] end

In this refined setup:

  • User access still comes through a mapped host port (e.g., 80) to the WebServer. This is the only entry point from outside the Docker host.
  • The WebServer and Database containers are now explicitly connected to App_Network.
  • Communication between WebServer and Database happens entirely within this isolated App_Network. This traffic never leaves the Docker host’s internal network interface, enhancing security.

Step-by-Step Implementation

We will modify our existing docker-compose.yml file to define a custom network and attach our services.

1. Define the Custom Network

Open your docker-compose.yml file (from the previous chapter). We will add a new top-level networks section.

Add the following to your docker-compose.yml file, typically at the end of the file, alongside the services and volumes (if any) sections:

# docker-compose.yml
# ... (existing services and volumes sections)

networks:
  app_network:
    driver: bridge

Explanation:

  • networks:: This is a top-level key in docker-compose.yml where you declare custom networks for your application.
  • app_network:: This is the name we’ve chosen for our custom network. It should be descriptive and unique within your Compose file.
  • driver: bridge: Specifies that Docker should create a standard bridge network. This is the most common type for single-host multi-container applications. It provides network isolation and internal DNS resolution, allowing containers on the same bridge network to communicate by their service names.

2. Attach Services to the Network

Now that we’ve defined app_network, we need to instruct our web and db services to connect to it. We do this by adding a networks key under each service definition.

Modify your docker-compose.yml to include these changes within the web and db service definitions:

# docker-compose.yml
services:
  web:
    build: .
    ports:
      - "80:80"
    environment:
      # ... (existing environment variables)
      DATABASE_HOST: db # Crucial: Use the service name for internal resolution
    networks:
      - app_network # Attach the web service to our custom network

  db:
    image: postgres:16-alpine # Using a specific, lightweight version as of 2026-05-22
    environment:
      POSTGRES_DB: mydatabase
      POSTGRES_USER: user
      POSTGRES_PASSWORD: password
    networks:
      - app_network # Attach the db service to our custom network
    # ... (existing volumes if any)

networks:
  app_network:
    driver: bridge

Explanation:

  • networks:: Under each service, this key lists the networks the service should connect to. A service can connect to multiple networks if needed, but for our current setup, one is sufficient.
  • - app_network: This line explicitly attaches both the web and db services to our app_network. This is a list item, so remember the hyphen.
  • DATABASE_HOST: db: This is a critical change. Because both services are now on app_network, Docker’s internal DNS will automatically resolve the service name db to the correct IP address of the db container within that network. This makes our application configuration robust and independent of dynamically assigned IP addresses.

3. Review the Complete docker-compose.yml

Your complete docker-compose.yml should now reflect these changes, looking similar to this:

# docker-compose.yml
# As of 2026-05-22, the Compose Specification recommends omitting the 'version' field.
# This allows Compose to use the latest specification.
# Reference: https://github.com/jamesatdocker/docker-docs/blob/main/compose/compose-file/compose-versioning.md

services:
  web:
    build: .
    ports:
      - "80:80"
    environment:
      APP_ENV: production
      DATABASE_HOST: db # Use the service name for internal resolution
      DATABASE_PORT: 5432 # Default PostgreSQL port
    networks:
      - app_network

  db:
    image: postgres:16-alpine # Using a specific, lightweight version as of 2026-05-22
    environment:
      POSTGRES_DB: mydatabase
      POSTGRES_USER: user
      POSTGRES_PASSWORD: password
    networks:
      - app_network
    # volumes:
    #   - db_data:/var/lib/postgresql/data # Uncomment if you set up volumes previously

# volumes:
#   db_data: # Uncomment if you set up volumes previously

networks:
  app_network:
    driver: bridge

⚡ Quick Note: The Docker Engine version is unknown as of 2026-05-22. For Docker Compose (Compose Specification), the best practice is to omit the version field from docker-compose.yml files. This ensures you’re using the latest specification without needing to update a version number manually.

Testing & Verification

After modifying the docker-compose.yml, it’s vital to verify that the network is created correctly and services are communicating as expected.

1. Rebuild and Start Services

First, stop any running containers from previous steps to ensure a clean slate, then bring up the new stack with the updated network configuration.

docker compose down --volumes --remove-orphans
docker compose up -d
  • docker compose down --volumes --remove-orphans: This command stops and removes containers, networks, and optionally volumes. The --volumes flag is important if you made changes that affect volumes (though not strictly necessary for just network changes), and --remove-orphans removes services that are no longer defined in the Compose file.
  • docker compose up -d: This command starts the services in detached mode (-d), creating the new app_network and attaching the specified services.

2. Verify Network Creation

Check if the app_network has been created by Docker.

docker network ls

You should see an entry similar to yourprojectname_app_network in the output. Docker Compose prefixes network names with the project directory name by default (e.g., if your project folder is my-app, the network might be my-app_app_network).

3. Inspect the Network

To see precisely which containers are attached to the network, use docker network inspect. Replace yourprojectname_app_network with the actual network name identified in the previous step.

docker network inspect yourprojectname_app_network

In the output, under the Containers section, you should see entries for both your web and db services, along with their assigned IP addresses within that network. This confirms they are correctly connected.

4. Test Inter-Service Communication

You can test if the web service can resolve and communicate with the db service using the ping command.

docker compose exec web ping db

You should see successful ping responses from the db container. This confirms that Docker’s internal DNS resolution is working correctly within app_network, and the web service can reach db by its service name.

If your web application connects to the database during startup, you should also check its logs for connection success:

docker compose logs web

Look for successful database connection messages and crucially, no errors related to DATABASE_HOST or connection refused.

5. Access the Application

Finally, ensure your web application is still accessible and fully functional via your browser at http://localhost. If your application has a database interaction (e.g., retrieving data or displaying a list from the database), verify that this functionality works correctly.

Production Considerations

  • Network Segmentation: For larger, more complex applications with many services, consider creating multiple custom networks. For instance, a frontend_network for web servers and load balancers, and a backend_network for application servers and databases. This further limits the blast radius if one segment is compromised and improves overall network security. 🧠 Important: Granular network segmentation is a key security practice, especially in microservices architectures.
  • Ingress/Egress Control: By default, custom bridge networks are very secure. The ports mapping in docker-compose.yml is the only way to expose a service to the host or external network. Be extremely mindful of which ports you expose and why. Never expose database ports (like 5432 for PostgreSQL) directly to the host or public network unless absolutely necessary and secured with strict firewall rules and authentication.
  • Host Firewall Rules: While Docker manages internal container networking, ensure your host’s firewall (e.g., ufw on Linux, Windows Defender Firewall) allows traffic to the host ports mapped by Docker Compose (e.g., port 80 for the web service).
  • Service Discovery: Docker’s internal DNS is excellent for service discovery within a Compose stack. Always use service names (like db) for inter-service communication within the docker-compose.yml file and your application code. Avoid hardcoding IP addresses, as they are ephemeral and can change.

Common Issues & Solutions

  1. Services cannot communicate (e.g., web can’t connect to db):
    • Cause: The most common reasons are that services are not on the same network, or the DATABASE_HOST (or similar environment variable) in the consuming service is incorrect.
    • Solution:
      • Double-check docker-compose.yml to ensure both services explicitly list app_network under their networks key with correct indentation.
      • Verify that environment variables in the consuming service (e.g., web) use the service name (e.g., db) and not localhost, 127.0.0.1, or an IP address.
      • Use docker network inspect yourprojectname_app_network to confirm both containers are listed.
      • Use docker compose exec [service_name] ping [target_service_name] to test basic connectivity and DNS resolution.
  2. docker network ls does not show the network after up:
    • Cause: docker compose up was not run after modifying the docker-compose.yml, or there’s a YAML syntax error in the networks section preventing Compose from parsing it correctly.
    • Solution:
      • Ensure you ran docker compose down followed by docker compose up -d.
      • Carefully review your docker-compose.yml for YAML syntax errors (e.g., incorrect indentation, missing colons). Even a single space can cause issues.
  3. Application fails to start with network-related errors (e.g., “connection refused”):
    • Cause: While defining networks helps with connectivity, it doesn’t solve startup order issues. A service might try to connect to a dependency (like a database) before the dependency is fully ready and listening for connections.
    • Solution: For now, check container logs for specific “connection refused” messages. We will address robust service startup order and readiness checks using Docker Compose health checks in a future chapter. For immediate debugging, a simple sleep command in your web service’s entrypoint or a retry logic in your application code can temporarily mitigate this.

Summary & Next Step

You’ve successfully established a secure and isolated network for your multi-service Docker Compose application. Your web and db services now communicate over a private bridge network, using Docker’s internal DNS for seamless service discovery. This is a critical step towards a robust and production-ready architecture, significantly enhancing both security and maintainability by isolating internal traffic.

Next, we’ll dive into managing persistent data with Docker volumes, ensuring that our database’s data survives container restarts, upgrades, and migrations, a non-negotiable requirement for any stateful production application.


This page is AI-assisted and reviewed. It references official documentation and recognized resources where relevant.

References