Multi-Service Apps with Container-to-Container Communication
In Part 7, we learned how your local browser can communicate with a containerized Django app. Today, we level up—connecting multiple containers (frontend, backend, and database) with Docker Compose. We'll explore inter-container communication, database persistence, healthchecks, and dependency management.
🎯 Learning Aims
By the end of this post, you'll be able to:
Connect multiple Docker containers together (container-to-container communication).
Use docker-compose.yml to manage a multi-service app (frontend, backend, database).
Understand and implement Docker healthchecks.
Control service start order and dependencies with depends_on.
Persist data using Docker volumes.
1. About the Project
This project is a full-stack web application composed of a frontend and a backend service, all running in separate Docker containers. The frontend is a Next.js application, and the backend is a Django application. A MySQL database is also included as a service. The entire application is orchestrated using Docker Compose, which allows for easy setup and management of the multi-container application.
📂 Project Structure
project/
├─ backend/
│ ├─ Dockerfile
│ ├─ requirements.txt
│ ├─ manage.py
│ └─ authz/
│ ├─ urls.py
│ └─ views.py
├─ frontend/
│ ├─ Dockerfile
│ └─ src/
│ ├─ Register.js
│ ├─ Login.js
│ └─ ForgotPw.js
├─ docker-compose.yml
└─ .env
🛢 Database (MySQL)
Image: Official MySQL (mysql:8.4)
Purpose: Persistent storage for user data and authentication.
Persist data: Docker named volume db_data.
🌐 Backend (Django API)
-
Routes:
- /register/: Create user accounts.
- /login/: Authenticate users, generate a token.
- /forgot-password/: Generate password reset codes.
Uses MySQL database to store and retrieve user information.
🖥 Frontend (Next.js UI)
-
API Calls:
- POST /register/
- POST /login/
- POST /forgot-password/
Communicates with backend to handle user authentication.
2. Docker Concepts to Learn
Frontend Dockerfile (frontend/Dockerfile
)
FROM node:20-alpine
WORKDIR /usr/src/app
COPY package*.json ./
RUN npm install
COPY . .
EXPOSE 3000
CMD ["npm","run","dev"]
-
FROM node:20-alpine
: This command specifies the base image for our container. We are using the official Node.js image with version 20 on an Alpine Linux distribution. Alpine is a lightweight Linux distribution, which results in a smaller image size. -
WORKDIR /usr/src/app
: This sets the working directory inside the container to/usr/src/app
. All subsequent commands will be executed from this directory. -
COPY package*.json ./
: This copies thepackage.json
andpackage-lock.json
files from the host machine to the current working directory (/usr/src/app
) inside the container. These files contain the project's dependencies. -
RUN npm install
: This command installs the dependencies listed in thepackage.json
file. -
COPY . .
: This copies the rest of the frontend application's source code into the container. -
EXPOSE 3000
: This informs Docker that the container will listen on port 3000 at runtime. -
CMD ["npm", "run", "dev"]
: This specifies the command to run when the container starts. In this case, it starts the Next.js development server.
Backend Dockerfile (backend/Dockerfile
)
FROM python:3.12-slim
WORKDIR /usr/src/app
RUN apt-get update && \
apt-get install -y --no-install-recommends \
build-essential \
default-libmysqlclient-dev \
pkg-config && \
rm -rf /var/lib/apt/lists/*
COPY requirements.txt ./
RUN pip install --upgrade pip && pip install -r requirements.txt
COPY . .
EXPOSE 8000
CMD ["python", "manage.py", "runserver", "0.0.0.0:8000"]
-
FROM python:3.12-slim
: This sets the base image to a slim version of Python 3.12. -
WORKDIR /usr/src/app
: Sets the working directory inside the container. -
RUN apt-get update && ...
: This command block updates the package lists and installs necessary packages for the application, such asbuild-essential
anddefault-libmysqlclient-dev
, which are required for building some of the Python dependencies.rm -rf /var/lib/apt/lists/*
is used to clean up and reduce the image size. -
COPY requirements.txt ./
: Copies therequirements.txt
file, which lists the Python dependencies. -
RUN pip install --upgrade pip && pip install -r requirements.txt
: Upgrades the package installer for Python and then installs the application's dependencies. -
COPY . .
: Copies the backend application's source code into the container. -
EXPOSE 8000
: Informs Docker that the container will listen on port 8000. -
CMD ["python", "manage.py", "runserver", "0.0.0.0:8000"]
: The command to run when the container starts, which is the Django development server.
Docker Compose (docker-compose.yml
)
services:
database:
image: mysql:8.4
container_name: mysql_db
env_file: .env
volumes:
- db_data:/var/lib/mysql
healthcheck:
test: ["CMD", "mysqladmin", "ping", "-h", "localhost", "-p${MYSQL_ROOT_PASSWORD}"]
interval: 5s
timeout: 3s
retries: 5
backend:
build: ./backend
container_name: django_api
env_file:
- .env
- ./backend/.env
command: python manage.py runserver 0.0.0.0:${BACKEND_PORT}
ports:
- "${BACKEND_PORT}:${BACKEND_PORT}"
volumes:
- ./backend:/usr/src/app
- /usr/local/lib/python3.12/site-packages
depends_on:
database:
condition: service_healthy
frontend:
build: ./frontend
container_name: next_ui
env_file:
- .env
- ./frontend/.env
ports:
- "${HOST_PORT_FRONTEND}:3000"
volumes:
- ./frontend:/usr/src/app
- /usr/src/app/node_modules
depends_on:
backend:
condition: service_started
volumes:
db_data: {}
-
services
: This is the main section where we define the different services that make up our application. -
database
: This is the name of our database service.-
image: mysql:8.4
: Specifies that this service will use the official MySQL image, version 8.4. -
container_name: mysql_db
: Assigns a specific name to the container for easier identification. -
env_file: .env
: Loads environment variables from a.env
file in the root directory. -
volumes: - db_data:/var/lib/mysql
: Mounts a named volumedb_data
to the/var/lib/mysql
directory in the container. This is to persist the database data even if the container is removed. -
healthcheck
: Defines a command to check if the database is healthy and ready to accept connections.
-
-
backend
: This is our Django backend service.-
build: ./backend
: Specifies that Docker Compose should build the image for this service using theDockerfile
in thebackend
directory. -
container_name: django_api
: A specific name for the backend container. -
env_file
: Loads environment variables from.env
files. -
command
: Overrides the default command in theDockerfile
. -
ports: - "${BACKEND_PORT}:${BACKEND_PORT}"
: Maps a port on the host machine to a port in the container. -
volumes
: Mounts the backend source code into the container for live reloading and creates an anonymous volume for the Python packages. -
depends_on
: Specifies that the backend service depends on thedatabase
service. It will wait until the database is healthy before starting.
-
-
frontend
: This is our Next.js frontend service.-
build: ./frontend
: Builds the image from thefrontend
directory'sDockerfile
. -
container_name: next_ui
: A specific name for the frontend container. -
ports: - "${HOST_PORT_FRONTEND}:3000"
: Maps the host port to the container's port 3000. -
volumes
: Mounts the frontend source code for live reloading and creates an anonymous volume fornode_modules
. -
depends_on
: The frontend service will start after the backend service has started.
-
-
volumes: db_data: {}
: Defines the named volumedb_data
used by thedatabase
service.
3. How to Run and Test the Project
To run the project, you need to have Docker and Docker Compose installed.
-
Build and run the containers:
docker-compose up --build
- **`docker-compose`**: The command-line tool for running multi-container Docker applications.
- **`up`**: This command builds, (re)creates, starts, and attaches to containers for a service.
- **`--build`**: This flag tells Docker Compose to build the images before starting the containers.
-
Access the application:
- Frontend: Open http://localhost:3000
- Register, Login, Forgot Password
- Backend: Test endpoints directly:
- Frontend: Open http://localhost:3000
-
To stop the application:
docker-compose down
- **`down`**: This command stops and removes the containers, networks, volumes, and images created by `up`.
4. Conclusion
From this project, you have learned:
- How to create
Dockerfile
s for both frontend (Node.js) and backend (Python/Django) applications. - How to use Docker Compose to define and run a multi-container application.
- How to connect services using Docker networks and
depends_on
. - How to use volumes to persist data and enable live code reloading.
- How to use environment variables to configure your application within Docker.
Congratulations on completing all eight parts of our Docker series! Let’s revisit the journey you’ve taken:
- Part 1 introduced container fundamentals and got you up and running with Docker CLI.
- Part 2 showed you how to package a Node/Express app into a container.
- Part 3 supercharged your development loop with live-reload and volumes.
- Part 4 taught you multi-stage builds to produce lean, production-ready images.
- Part 5 walked you through debugging techniques inside an active container.
- Part 6 demonstrated secure, flexible configuration management via environment variables.
- Part 7 combined your front-end and back-end into a cohesive Docker Compose setup.
- Part 8 culminated in a multi-service architecture, exploring custom networks and service orchestration.
You now have a solid foundation in containerizing applications, optimizing images, debugging, and composing complex environments. From here, you might explore:
- CI/CD with Docker: Automate builds and deployments in Jenkins, GitHub Actions, or GitLab CI.
- Container Security: Scan images for vulnerabilities and follow best practices to lock down your containers.
- Orchestration with Kubernetes: Take your Docker skills to production scale with Pods, Deployments, and Services.
Thank you for joining this series—happy Dockering, and may your containers always start on the first try! 🚀
Top comments (0)