Appearance
Docker Deployment
This document provides a comprehensive guide to containerizing and deploying the IT Ticketing Service using Docker and Docker Compose. It is intended for developers responsible for local development, testing, and maintenance of the application's containerized environment.
Containerization is a core part of our deployment strategy, enabling consistent environments from local development to production. It also serves as a critical prerequisite for cloud migration strategies, as detailed in the GCP Migration documentation.
Docker Compose
For local development, we use Docker Compose to orchestrate the application's dependencies: a MySQL database and a RabbitMQ message broker. The configuration is defined in the docker-compose.yml file at the root of the project. This approach simplifies the setup process, ensuring all developers work with identical, isolated dependency versions.
For instructions on running the full stack locally, refer to the Running Locally guide.
MySQL Container Configuration
The mysql service provides a dedicated database instance for the application.
yaml
# docker-compose.yml
services:
mysql:
image: mysql:8.0
container_name: ticketing-mysql
environment:
MYSQL_ROOT_PASSWORD: password
MYSQL_DATABASE: ticketing
MYSQL_USER: ticketing_user
MYSQL_PASSWORD: changeme
ports:
- "3306:3306"
volumes:
- mysql-data:/var/lib/mysql
healthcheck:
# ... (covered in Health Checks section)1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Configuration Breakdown:
image: mysql:8.0: Specifies the official MySQL image, version 8.0. Pinning the version ensures consistency across all developer environments.environment: Configures the database upon initialization.MYSQL_DATABASE: ticketing: Creates a database namedticketing.MYSQL_USER/MYSQL_PASSWORD: Creates a dedicated user with credentials for the application to use, adhering to the principle of least privilege.
ports: ["3306:3306"]: Maps port 3306 on the host machine to port 3306 inside the container. This allows database management tools (like DBeaver or MySQL Workbench) on your local machine to connect to the containerized database.volumes: [mysql-data:/var/lib/mysql]: Mounts a named volumemysql-datato the container's data directory. This ensures that all database data persists even if the container is stopped, removed, or recreated.
RabbitMQ Container Configuration
The rabbitmq service provides the AMQP message broker for the application's event-driven features.
yaml
# docker-compose.yml
services:
rabbitmq:
image: rabbitmq:3.12-management
container_name: ticketing-rabbitmq
environment:
RABBITMQ_DEFAULT_USER: guest
RABBITMQ_DEFAULT_PASS: guest
ports:
- "5672:5672" # AMQP port
- "15672:15672" # Management UI
healthcheck:
# ... (covered in Health Checks section)1
2
3
4
5
6
7
8
9
10
11
12
13
2
3
4
5
6
7
8
9
10
11
12
13
Configuration Breakdown:
image: rabbitmq:3.12-management: Uses the official RabbitMQ image, specifically themanagementtag, which includes the helpful RabbitMQ Management Plugin web UI.ports:5672:5672: Exposes the standard AMQP protocol port for the application to connect to.15672:15672: Exposes the management UI, which can be accessed in a browser athttp://localhost:15672(usingguest/guestcredentials). This is invaluable for debugging message queues and exchanges.
Volume Management for Persistence
A top-level volumes key is defined to manage the named volume used by MySQL.
yaml
# docker-compose.yml
volumes:
mysql-data:1
2
3
2
3
This explicitly declares the mysql-data volume, which Docker manages. Using a named volume is preferable to a host-path mount for database data in development, as it abstracts away the host's file system and handles permissions automatically.
Health Checks
The docker-compose.yml file includes health checks for both the database and the message broker. These checks are crucial for startup orchestration, allowing dependent services (like the application itself) to wait until a dependency is not just running, but fully ready to accept connections.
MySQL Health Check
The MySQL service health check pings the server to ensure it's responsive.
yaml
# docker-compose.yml (inside mysql service)
healthcheck:
test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]
timeout: 20s
retries: 101
2
3
4
5
2
3
4
5
test: The commandmysqladmin pingis a lightweight way to verify that the MySQL daemon is alive and listening for connections.timeout: The check will fail if the command takes longer than 20 seconds.retries: Docker will attempt the check up to 10 times before marking the container asunhealthy. This grace period is essential as the database can take several seconds to initialize.
RabbitMQ Health Check
The RabbitMQ health check uses a built-in diagnostic tool.
yaml
# docker-compose.yml (inside rabbitmq service)
healthcheck:
test: ["CMD", "rabbitmq-diagnostics", "ping"]
timeout: 20s
retries: 101
2
3
4
5
2
3
4
5
test: Therabbitmq-diagnostics pingcommand checks if the RabbitMQ Erlang node is running and responsive.
Service Startup Dependencies
While the provided docker-compose.yml only defines the dependencies, a complete local setup would also include the application service. In that scenario, you would use depends_on to control startup order and ensure the application only starts after its dependencies are healthy.
Example docker-compose.yml with Application Service:
yaml
# Example for a full stack setup
services:
# ... mysql and rabbitmq services ...
app:
build: .
ports:
- "8080:8080"
environment:
- SPRING_DATASOURCE_URL=jdbc:mysql://mysql:3306/ticketing
- SPRING_RABBITMQ_HOST=rabbitmq
depends_on:
mysql:
condition: service_healthy
rabbitmq:
condition: service_healthy1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
The condition: service_healthy is key; it instructs Docker Compose to wait for the mysql and rabbitmq health checks to pass before starting the app container.
Container Networking
Docker Compose simplifies container networking by creating a default bridge network and connecting all services defined in the file to it.
Service Discovery
Within this internal network, containers can find each other using their service names as hostnames. This is the standard and recommended way for containers to communicate.
- The application container can connect to MySQL using the hostname
mysql. - The application container can connect to RabbitMQ using the hostname
rabbitmq.
Gotcha: The
application.propertiesfile in the source code is configured for native local development (e.g.,spring.rabbitmq.host=localhost). When running the application inside a Docker container, these properties must be overridden to use the service names. The most common method is via environment variables, as shown in the "Service Startup Dependencies" example above.
Port Mappings
Port mappings (host:container) expose container ports to the host machine's network. This is primarily for external access—from your browser, a database client, or an API tool like Postman.
| Service | Mapping | Purpose |
|---|---|---|
mysql | 3306:3306 | Access the database from a client on the host. |
rabbitmq | 5672:5672 | (Optional) Connect a local app to the container. |
rabbitmq | 15672:15672 | Access the RabbitMQ Management UI from a browser. |
app | 8080:8080 | Access the application's REST API from the host. |
Internal communication between containers (e.g., app to mysql) does not use these mapped ports; it occurs directly over the internal Docker network on the container's native port (e.g., mysql:3306).
Building Application Container
To run the Spring Boot application as a container, you need a Dockerfile. This file defines the steps to build a portable container image containing the application and its runtime environment.
Creating a Dockerfile
We use a multi-stage build, which is a best practice for creating optimized, secure, and small container images.
Dockerfile
dockerfile
# Stage 1: Build the application using Maven
FROM maven:3.9-eclipse-temurin-17 AS build
WORKDIR /app
# Copy the pom.xml and download dependencies first to leverage Docker layer caching
COPY pom.xml .
RUN mvn dependency:go-offline
# Copy the rest of the source code and build the application
COPY src ./src
RUN mvn clean package -DskipTests
# Stage 2: Create the final, lightweight runtime image
FROM eclipse-temurin:17-jre-jammy
WORKDIR /app
# Create a non-root user for security
RUN groupadd --system appuser && useradd --system --gid appuser appuser
USER appuser
# Copy only the built JAR from the 'build' stage
COPY --from=build /app/target/*.jar app.jar
# Expose the port the application runs on
EXPOSE 8080
# Command to run the application
ENTRYPOINT ["java", "-jar", "app.jar"]1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
Multi-Stage Build Explained
buildStage:- Starts from a full Maven and JDK image (
maven:3.9-eclipse-temurin-17). - It first copies only
pom.xmland runsmvn dependency:go-offline. This step downloads all dependencies into a separate layer. Becausepom.xmlchanges less frequently than source code, this layer is often cached by Docker, speeding up subsequent builds significantly. - It then copies the application source (
src) and runsmvn clean packageto compile the code and create the executable.jarfile.
- Starts from a full Maven and JDK image (
Runtime Stage:
- Starts from a minimal Java Runtime Environment (JRE) image (
eclipse-temurin:17-jre-jammy), which is much smaller than a full JDK. - It copies only the
app.jarfile from thebuildstage. None of the source code, Maven build files, or intermediate artifacts are included in the final image. - This results in a small, production-ready image containing only what's necessary to run the application.
- Starts from a minimal Java Runtime Environment (JRE) image (
Container Best Practices
The provided Dockerfile and our overall strategy incorporate several containerization best practices:
- Use Specific Base Image Tags: We use
mysql:8.0andeclipse-temurin:17-jre-jammyinstead oflatestto ensure builds are reproducible and avoid unexpected breaking changes. - Run as a Non-Root User: The runtime stage creates and switches to a dedicated
appuser. This is a critical security measure that limits the potential damage if the application process is compromised. - Optimize Layer Caching: By copying
pom.xmland downloading dependencies before the source code, we maximize Docker's layer caching and accelerate build times. - Create a
.dockerignorefile: To prevent unnecessary files from being sent to the Docker daemon and invalidating the build cache, a.dockerignorefile should be present in the project root.
Example .dockerignore
.git
.gitignore
.idea/
target/
*.iml1
2
3
4
5
2
3
4
5