Skip to content

Commit 505dcbe

Browse files
feat: Add docker-compose (#6)
* feat: add docker-compose * chore: complete kafka setup for docker-compose * chore: provision topics * chore: setup docker-compose LB for API * chore: describe topics * feat: add startup URL to log output * Get user-management service running * can't timeout and stopping token --------- Co-authored-by: james.eastham <james.eastham@datadoghq.com>
1 parent e853fad commit 505dcbe

8 files changed

Lines changed: 297 additions & 15 deletions

File tree

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
11

22
**/.claude/settings.local.json
3+
.cursor

README.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,3 +17,13 @@ In both cases these will be modelled in OpenAPI.
1717
| [User Management](./user-management/) | Manages user accounts, authentication, and profile information. Handles user registration, login, and JWT token issuance. | [API Docs](./user-management/docs/api.json) |
1818
| [Sticker Award](./sticker-award/) | Manages the assignment of stickers to users. Tracks which users have which stickers and handles assignment/removal based on criteria like certification completion. | [API Docs](./sticker-award/docs/api.json) |
1919

20+
## Messaging Topics
21+
22+
The following Kafka topics are used for asynchronous communication between services:
23+
24+
| Topic Name | Publishing Service | Description |
25+
|------------|-------------------|-------------|
26+
| users.userRegistered.v1 | User Management | Published when a new user successfully registers. Contains user profile information for other services to initialize user-related data. |
27+
| users.userDetailsUpdated.v1 | User Management | Published when a user updates their profile information. Contains the updated user profile data. |
28+
| users.stickerClaimed.v1 | TODO | TODO |
29+

docker-compose.yml

Lines changed: 181 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,181 @@
1+
version: '3.8'
2+
3+
# Stickerlandia Service Endpoints
4+
# -------------------------------
5+
# - Sticker Award API: http://localhost:8080/api/award
6+
# - User stickers: http://localhost:8080/api/award/v1/users/user-001/stickers
7+
#
8+
# - User Management API: http://localhost:8080/api/users
9+
# - Registration: http://localhost:8080/api/users/v1/register
10+
# - Login: http://localhost:8080/api/users/v1/login
11+
#
12+
# - Traefik Dashboard: http://localhost:8081/dashboard/
13+
# - Redpanda Console: http://localhost:8082
14+
#
15+
services:
16+
traefik:
17+
image: traefik:v3.0
18+
container_name: traefik
19+
ports:
20+
- "8080:80" # App traffic
21+
- "8081:8080" # Dashboard
22+
command:
23+
- --api.dashboard=true
24+
- --api.insecure=true
25+
- --entrypoints.web.address=:80
26+
- --entrypoints.dashboard.address=:8081
27+
- --providers.docker=true
28+
- --providers.docker.exposedbydefault=false
29+
volumes:
30+
- /var/run/docker.sock:/var/run/docker.sock:ro
31+
labels:
32+
- "traefik.enable=true"
33+
- "traefik.http.routers.traefik.rule=PathPrefix(`/dashboard`)"
34+
- "traefik.http.routers.traefik.service=api@internal"
35+
- "traefik.http.routers.traefik.entrypoints=dashboard"
36+
37+
redpanda:
38+
image: redpandadata/redpanda:latest
39+
container_name: stickerlandia-redpanda
40+
command:
41+
- redpanda
42+
- start
43+
- --smp=1
44+
- --memory=1G
45+
- --reserve-memory=0M
46+
- --overprovisioned
47+
- --node-id=0
48+
- --check=false
49+
- --pandaproxy-addr=0.0.0.0:8082
50+
- --advertise-pandaproxy-addr=redpanda:8082
51+
- --kafka-addr=0.0.0.0:9092
52+
- --advertise-kafka-addr=redpanda:9092
53+
- --rpc-addr=0.0.0.0:33145
54+
- --advertise-rpc-addr=redpanda:33145
55+
- --mode dev-container
56+
ports:
57+
- "9092:9092"
58+
# - "8082:8082"
59+
- "9644:9644"
60+
volumes:
61+
- redpanda-data:/var/lib/redpanda/data
62+
healthcheck:
63+
test: ["CMD", "rpk", "cluster", "health"]
64+
interval: 10s
65+
timeout: 5s
66+
retries: 5
67+
68+
redpanda-init:
69+
image: redpandadata/redpanda:latest
70+
container_name: redpanda-init
71+
depends_on:
72+
redpanda:
73+
condition: service_healthy
74+
entrypoint: ["/bin/sh", "-c"]
75+
command: >
76+
"rpk topic create users.userRegistered.v1 users.stickerClaimed.v1 users.userDetailsUpdated.v1 --brokers redpanda:9092"
77+
restart: "no"
78+
79+
redpanda-console:
80+
image: redpandadata/console:latest
81+
container_name: stickerlandia-redpanda-console
82+
depends_on:
83+
redpanda:
84+
condition: service_healthy
85+
ports:
86+
- "8082:8080"
87+
environment:
88+
KAFKA_BROKERS: redpanda:9092
89+
# REDPANDA_ADMIN_API_URLS: http://redpanda:9644
90+
healthcheck:
91+
test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost:8080"]
92+
interval: 10s
93+
timeout: 5s
94+
retries: 5
95+
96+
sticker-award-db:
97+
image: postgres:16
98+
container_name: sticker-award-db
99+
environment:
100+
POSTGRES_DB: sticker_award
101+
POSTGRES_USER: sticker_user
102+
POSTGRES_PASSWORD: sticker_password
103+
ports:
104+
- "5432:5432"
105+
volumes:
106+
- sticker-award-data:/var/lib/postgresql/data
107+
healthcheck:
108+
test: ["CMD-SHELL", "pg_isready -U sticker_user -d sticker_award"]
109+
interval: 5s
110+
timeout: 5s
111+
retries: 5
112+
113+
user-management-db:
114+
image: postgres:16
115+
container_name: user-management-db
116+
environment:
117+
POSTGRES_DB: user_management
118+
POSTGRES_USER: user_mgmt_user
119+
POSTGRES_PASSWORD: user_mgmt_password
120+
ports:
121+
- "5433:5432"
122+
volumes:
123+
- user-management-data:/var/lib/postgresql/data
124+
healthcheck:
125+
test: ["CMD-SHELL", "pg_isready -U user_mgmt_user -d user_management"]
126+
interval: 5s
127+
timeout: 5s
128+
retries: 5
129+
130+
sticker-award:
131+
build:
132+
context: ./sticker-award
133+
dockerfile: src/main/docker/Dockerfile.jvmlocal
134+
container_name: sticker-award
135+
depends_on:
136+
sticker-award-db:
137+
condition: service_healthy
138+
redpanda:
139+
condition: service_healthy
140+
environment:
141+
QUARKUS_DATASOURCE_JDBC_URL: jdbc:postgresql://sticker-award-db:5432/sticker_award
142+
QUARKUS_DATASOURCE_USERNAME: sticker_user
143+
QUARKUS_DATASOURCE_PASSWORD: sticker_password
144+
QUARKUS_DATASOURCE_DB_KIND: postgresql
145+
QUARKUS_DATASOURCE_DEVSERVICES_ENABLED: "false"
146+
KAFKA_BOOTSTRAP_SERVERS: redpanda:9092
147+
MP_MESSAGING_CONNECTOR_SMALLRYE_KAFKA_BOOTSTRAP_SERVERS: redpanda:9092
148+
QUARKUS_KAFKA_STREAMS_BOOTSTRAP_SERVERS: redpanda:9092
149+
labels:
150+
- "traefik.enable=true"
151+
- "traefik.http.routers.sticker-award.rule=PathPrefix(`/api/award`)"
152+
- "traefik.http.services.sticker-award.loadbalancer.server.port=8080"
153+
154+
user-management:
155+
build:
156+
context: ./user-management
157+
dockerfile: src/Stickerlandia.UserManagement.AspNet/Dockerfile
158+
container_name: user-management
159+
depends_on:
160+
user-management-db:
161+
condition: service_healthy
162+
redpanda:
163+
condition: service_healthy
164+
environment:
165+
ASPNETCORE_ENVIRONMENT: Development
166+
DRIVING: ASPNET
167+
DRIVEN: AGNOSTIC
168+
ConnectionStrings__messaging: "redpanda:9092"
169+
ConnectionStrings__database: "Host=user-management-db;Port=5432;Database=user_management;Username=user_mgmt_user;Password=user_mgmt_password"
170+
KAFKA__BOOTSTRAPSERVERS: "redpanda:9092"
171+
KAFKA__SCHEMAREGISTRY: "http://redpanda:8082"
172+
KAFKA__GROUPID: "stickerlandia-user-management"
173+
labels:
174+
- "traefik.enable=true"
175+
- "traefik.http.routers.user-management.rule=PathPrefix(`/api/users`)"
176+
- "traefik.http.services.user-management.loadbalancer.server.port=8080"
177+
178+
volumes:
179+
sticker-award-data:
180+
user-management-data:
181+
redpanda-data:

sticker-award/.dockerignore

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
1-
*
21
!target/*-runner
32
!target/*-runner.jar
43
!target/lib/*
5-
!target/quarkus-app/*
4+
!target/quarkus-app/*
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
####
2+
# Multi-stage Dockerfile that builds and runs the Quarkus application in JVM mode
3+
# This version includes the build stage, eliminating the need to run ./mvnw package separately.
4+
#
5+
# Build the image with:
6+
#
7+
# docker build -f src/main/docker/Dockerfile.jvmlocal -t quarkus/sticker-award-jvm .
8+
#
9+
# Then run the container using:
10+
#
11+
# docker run -i --rm -p 8080:8080 quarkus/sticker-award-jvm
12+
#
13+
# If you want to include the debug port into your docker image
14+
# you will have to expose the debug port (default 5005 being the default) like this: EXPOSE 8080 5005.
15+
# Additionally you will have to set -e JAVA_DEBUG=true and -e JAVA_DEBUG_PORT=*:5005
16+
# when running the container
17+
###
18+
19+
# Stage 1: Build stage
20+
FROM maven:3.9-eclipse-temurin-21 AS build
21+
WORKDIR /build
22+
23+
# The context needs to be the root of the sticker-award directory, not the Dockerfile location
24+
COPY pom.xml .
25+
COPY src ./src
26+
27+
# Make sure mvnw is executable and then build
28+
RUN mvn package -DskipTests
29+
30+
# Stage 2: Runtime stage
31+
FROM registry.access.redhat.com/ubi9/openjdk-21:1.21
32+
33+
ENV LANGUAGE='en_US:en'
34+
35+
# We make four distinct layers so if there are application changes the library layers can be re-used
36+
COPY --from=build --chown=185 /build/target/quarkus-app/lib/ /deployments/lib/
37+
COPY --from=build --chown=185 /build/target/quarkus-app/*.jar /deployments/
38+
COPY --from=build --chown=185 /build/target/quarkus-app/app/ /deployments/app/
39+
COPY --from=build --chown=185 /build/target/quarkus-app/quarkus/ /deployments/quarkus/
40+
41+
EXPOSE 8080
42+
USER 185
43+
ENV JAVA_OPTS_APPEND="-Dquarkus.http.host=0.0.0.0 -Djava.util.logging.manager=org.jboss.logmanager.LogManager"
44+
ENV JAVA_APP_JAR="/deployments/quarkus-run.jar"
45+
46+
ENTRYPOINT [ "/opt/jboss/container/java/run/run-java.sh" ]

user-management/src/Stickerlandia.UserManagement.Agnostic/KafakStickerClaimedWorker.cs

Lines changed: 31 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -58,29 +58,54 @@ public Task StartAsync()
5858

5959
public async Task PollAsync(CancellationToken stoppingToken)
6060
{
61+
// Check for cancellation first
62+
if (stoppingToken.IsCancellationRequested)
63+
return;
64+
6165
using var scope = _serviceScopeFactory.CreateScope();
6266
var handler = scope.ServiceProvider.GetRequiredService<StickerClaimedEventHandler>();
6367

64-
using (var consumer = new ConsumerBuilder<string, string>(_consumerConfig).Build())
68+
try
6569
{
70+
using var consumer = new ConsumerBuilder<string, string>(_consumerConfig)
71+
.SetErrorHandler((_, e) => _logger.LogError("Kafka error: {Error}", e.Reason))
72+
.Build();
73+
6674
consumer.Subscribe(topic);
75+
6776
try {
68-
var cr = consumer.Consume(stoppingToken);
77+
var consumeResult = consumer.Consume(TimeSpan.FromSeconds(2));
6978

70-
var processResult = await ProcessMessageAsync(handler, cr);
71-
72-
if (processResult)
79+
if (consumeResult != null)
7380
{
74-
consumer.Commit(cr);
81+
var processResult = await ProcessMessageAsync(handler, consumeResult);
82+
83+
if (processResult)
84+
{
85+
consumer.Commit(consumeResult);
86+
}
7587
}
7688
}
89+
catch (ConsumeException ex) {
90+
_logger.LogError(ex, "Error consuming message");
91+
}
92+
catch (OperationCanceledException)
93+
{
94+
// This is expected when the token is canceled
95+
_logger.LogInformation("Polling canceled");
96+
}
7797
catch (Exception ex) {
7898
_logger.LogError(ex, "Error processing message");
7999
}
80100
finally{
101+
// Ensure consumer is closed even if an exception occurs
81102
consumer.Close();
82103
}
83104
}
105+
catch (Exception ex)
106+
{
107+
_logger.LogError(ex, "Failed to create Kafka consumer");
108+
}
84109
}
85110

86111
public Task StopAsync(CancellationToken cancellationToken)
Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,19 @@
1-
FROM mcr.microsoft.com/dotnet/sdk:9.0 as build
1+
FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build
22

33
WORKDIR /App
4+
5+
# Deps
6+
COPY src/Stickerlandia.UserManagement.AspNet/Stickerlandia.UserManagement.AspNet.csproj src/Stickerlandia.UserManagement.AspNet/
7+
RUN dotnet restore src/Stickerlandia.UserManagement.AspNet/Stickerlandia.UserManagement.AspNet.csproj
8+
9+
# Now copy the rest of the source code
410
COPY . ./
5-
RUN dotnet restore PlantBasedPizza.Account/application/PlantBasedPizza.Account.Api/PlantBasedPizza.Account.Api.csproj
6-
RUN dotnet publish PlantBasedPizza.Account/application/PlantBasedPizza.Account.Api/PlantBasedPizza.Account.Api.csproj -o out -c Release -r linux-arm64
711

8-
FROM mcr.microsoft.com/dotnet/aspnet:9.0-noble-chiseled as runtime
12+
# Publish the application
13+
RUN dotnet publish src/Stickerlandia.UserManagement.AspNet/Stickerlandia.UserManagement.AspNet.csproj -o out -c Release -r linux-arm64
14+
15+
FROM mcr.microsoft.com/dotnet/aspnet:8.0-noble-chiseled AS runtime
916
WORKDIR /App
1017
COPY --from=build /App/out .
1118
EXPOSE 80
12-
ENTRYPOINT [ "dotnet", "PlantBasedPizza.Account.Api.dll" ]
19+
ENTRYPOINT [ "dotnet", "Stickerlandia.UserManagement.AspNet.dll" ]

user-management/src/Stickerlandia.UserManagement.AspNet/Program.cs

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,6 @@
2020

2121
var logger = Log.Logger = new LoggerConfiguration()
2222
.MinimumLevel.Information()
23-
.MinimumLevel.Override("Microsoft", LogEventLevel.Error)
2423
.Enrich.FromLogContext()
2524
.WriteTo.Console(new JsonFormatter())
2625
.CreateLogger();
@@ -140,7 +139,21 @@
140139
await database.MigrateAsync();
141140
}
142141

143-
await app.RunAsync();
142+
try
143+
{
144+
await app.StartAsync().WaitAsync(TimeSpan.FromSeconds(30)); // Add timeout to prevent hanging indefinitely
145+
146+
var urlList = app.Urls;
147+
var urls = string.Join(" ", urlList);
148+
149+
logger.Information("UserManagement API started on {Urls}", urls);
150+
}
151+
catch (Exception ex)
152+
{
153+
logger.Error(ex, "Error starting the application");
154+
}
155+
156+
await app.WaitForShutdownAsync();
144157

145158
static Task WriteHealthCheckResponse(HttpContext context, HealthReport healthReport)
146159
{

0 commit comments

Comments
 (0)