Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# =============================================================================
# Environment Variables for docker-compose-all.yml
# =============================================================================
# Copy this file to .env and customize values for your environment.
#
# Usage:
# cp .env.example .env
# # Edit .env with your values
# docker compose -f docker-compose-all.yml up -d
#
# SECURITY WARNING:
# - Never commit .env to version control
# - Use secrets management (Vault, AWS Secrets Manager) in production
# =============================================================================

# Database password (used by both app and postgres containers)
DB_PASSWORD=your_secure_password_here

# JWT secrets - MUST be at least 64 bytes for HS512
# Generate with: openssl rand -base64 64
JWT_ACCESS_SECRET=your-64-byte-access-secret-generated-with-openssl-rand-base64-64-command
JWT_REFRESH_SECRET=your-64-byte-refresh-secret-generated-with-openssl-rand-base64-64-command

# CORS allowed origins (comma-separated for multiple)
CORS_ALLOWED_ORIGINS=http://localhost:3000
7 changes: 6 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,9 @@ build
target
.DS_Store
logs
!auto/build
!auto/build

# Environment files (contain secrets)
.env
.env.local
.env.*.local
58 changes: 46 additions & 12 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -1,31 +1,65 @@
# Build stage
FROM openjdk:21-jdk-slim AS build
# =============================================================================
# Multi-stage Dockerfile for Spring Boot Application
# =============================================================================
# Build: docker build -t user-service .
# Run: docker run -p 3001:3001 -p 9090:9090 user-service
# =============================================================================

# -----------------------------------------------------------------------------
# Build Stage (use Eclipse Temurin with glibc for protoc compatibility)
# -----------------------------------------------------------------------------
FROM eclipse-temurin:25-jdk AS build

WORKDIR /app

# Copy Gradle wrapper and buf-gen files
# Copy Gradle wrapper and build files first (for layer caching)
COPY gradle/ gradle/
COPY gradlew build.gradle.kts ./

# Make gradlew executable
RUN chmod +x ./gradlew

# Copy source code
# Download dependencies (cached layer if build files unchanged)
RUN ./gradlew dependencies --no-daemon || true

# Copy source code (including proto files)
COPY src ./src

# Build the application
# Build the application (generateProto runs automatically, skip tests)
RUN ./gradlew clean bootJar -x test --no-daemon

# Runtime stage
FROM eclipse-temurin:21-jre
# -----------------------------------------------------------------------------
# Runtime Stage (use Alpine for smaller image size)
# -----------------------------------------------------------------------------
FROM eclipse-temurin:25-jre-alpine

# Add non-root user for security
RUN addgroup -g 1001 -S appgroup && \
adduser -u 1001 -S appuser -G appgroup

WORKDIR /app

# Copy JAR from buf-gen stage
COPY --from=build /app/build/libs/user-service.jar user-service.jar
# Copy JAR from build stage (matches bootJar archiveFileName in build.gradle.kts)
COPY --from=build /app/build/libs/user-application.jar app.jar

# Change ownership to non-root user
RUN chown -R appuser:appgroup /app

# Switch to non-root user
USER appuser

# Expose REST and gRPC ports
EXPOSE 3001 9090

# Health check
HEALTHCHECK --interval=30s --timeout=10s --start-period=60s --retries=3 \
CMD wget --no-verbose --tries=1 --spider http://localhost:3001/actuator/health || exit 1

# Expose port
EXPOSE 3001
# JVM optimizations for containers
ENV JAVA_OPTS="-XX:+UseContainerSupport \
-XX:MaxRAMPercentage=75.0 \
-XX:InitialRAMPercentage=50.0 \
-Djava.security.egd=file:/dev/./urandom"

# Run the application
CMD ["java", "-jar", "user-service.jar"]
ENTRYPOINT ["sh", "-c", "java $JAVA_OPTS -jar app.jar"]
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,8 @@ A comprehensive user authentication and management service built with Java 21 an

## Tech Stack

- **Java 21** (Latest LTS)
- **Spring Boot 3.5.3**
- **Java 25** (Latest LTS)
- **Spring Boot 4**
- **Spring Security 6**
- **Spring Data JPA**
- **PostgreSQL 17**
Expand Down
3 changes: 3 additions & 0 deletions auto/docker_logs
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
#!/usr/bin/env sh

docker compose -f docker-compose-all.yml up -
File renamed without changes.
3 changes: 3 additions & 0 deletions auto/docker_stop
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
#!/usr/bin/env sh

docker compose -f docker-compose-all.yml down
7 changes: 0 additions & 7 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ plugins {
id("io.spring.dependency-management") version "1.1.7"
id("org.graalvm.buildtools.native") version "0.11.1"
id("org.jetbrains.kotlin.jvm") version "2.2.21"
id("io.swagger.core.v3.swagger-gradle-plugin") version "2.2.34"
id("org.flywaydb.flyway") version "11.11.1"
id("com.diffplug.spotless") version "8.1.0"
id("com.google.protobuf") version "0.9.5"
Expand Down Expand Up @@ -64,12 +63,6 @@ dependencies {
testImplementation("org.testcontainers:junit-jupiter")
testImplementation("org.testcontainers:postgresql")

// Swagger
implementation("io.swagger.core.v3:swagger-models:2.2.34")
implementation("io.swagger.core.v3:swagger-core:2.2.34")
implementation("jakarta.xml.bind:jakarta.xml.bind-api:4.0.2")
runtimeOnly("org.glassfish.jaxb:jaxb-runtime:4.0.5")

// gRPC and Protobuf
implementation("io.grpc:grpc-netty-shaded:1.77.0")
implementation("io.grpc:grpc-protobuf:1.77.0")
Expand Down
106 changes: 87 additions & 19 deletions docker-compose-all.yml
Original file line number Diff line number Diff line change
@@ -1,41 +1,109 @@
# =============================================================================
# Docker Compose - Full Application Stack
# =============================================================================
# Simulates dev/prod environment with all services running in containers.
#
# Usage:
# Start: docker compose -f docker-compose-all.yml up -d
# Stop: docker compose -f docker-compose-all.yml down
# Logs: docker compose -f docker-compose-all.yml logs -f user-service
# Rebuild: docker compose -f docker-compose-all.yml up -d --build
#
# For production, use external secrets management (Vault, AWS Secrets Manager)
# instead of environment variables in this file.
# =============================================================================

services:
# ---------------------------------------------------------------------------
# Application Service
# ---------------------------------------------------------------------------
user-service:
build:
context: .
dockerfile: Dockerfile
container_name: user-application
ports:
- "3001:3001"
- "3001:3001" # REST API
- "9090:9090" # gRPC API
depends_on:
- postgres
postgres:
condition: service_healthy
environment:
- SPRING_PROFILES_ACTIVE=docker
- SPRING_DATASOURCE_URL=jdbc:postgresql://postgres:5432/users
- SPRING_DATASOURCE_USERNAME=test_user_rw
- SPRING_DATASOURCE_PASSWORD=test_user@pass01
- JWT_ACCESS_SECRET=13483567-651e-410a-b79e-d4c1a66d3bd526b90300-0d1e-4d92-85e0-1747241d052d
- JWT_REFRESH_SECRET=8878f2cc-616e-413a-9b72-2eb32d7bf55c332df003-da39-45b7-82b1-f5f0d3203375
- CLIENT_URL=http://localhost:3000
volumes:
- ./logs:/app/logs
# Profile: use 'dev' for development simulation, 'prod' for production
- SPRING_PROFILES_ACTIVE=dev

# Database connection (matches dev/prod profile expectations)
- DATABASE_URL=jdbc:postgresql://postgres:5432/users
- DATABASE_USERNAME=app_user
- DATABASE_PASSWORD=${DB_PASSWORD:-changeme_in_production}

# JWT secrets - MUST be overridden in production!
# Generate with: openssl rand -base64 64
- JWT_ACCESS_SECRET=${JWT_ACCESS_SECRET:-dev-only-access-secret-key-must-be-at-least-64-bytes-long-for-hs512}
- JWT_REFRESH_SECRET=${JWT_REFRESH_SECRET:-dev-only-refresh-secret-key-must-be-at-least-64-bytes-long-for-hs512}

# CORS - adjust for your frontend URL
- CORS_ALLOWED_ORIGINS=${CORS_ALLOWED_ORIGINS:-http://localhost:3000}

# JVM options for container environment
- JAVA_OPTS=-XX:+UseContainerSupport -XX:MaxRAMPercentage=75.0
healthcheck:
test: [ "CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost:3001/actuator/health" ]
interval: 30s
timeout: 10s
retries: 3
start_period: 60s
restart: unless-stopped
deploy:
resources:
limits:
cpus: '2'
memory: 1G
reservations:
cpus: '0.5'
memory: 512M
networks:
- user-network
- app-network

# ---------------------------------------------------------------------------
# PostgreSQL Database
# ---------------------------------------------------------------------------
postgres:
image: postgres:17.5-alpine
image: postgres:17-alpine
container_name: user-application-db
ports:
- "54321:5432"
- "54321:5432" # External port for debugging (remove in production)
environment:
- POSTGRES_DB=users
- POSTGRES_USER=test_user_rw
- POSTGRES_PASSWORD=test_user@pass01
- POSTGRES_USER=app_user
- POSTGRES_PASSWORD=${DB_PASSWORD:-changeme_in_production}
# Performance tuning for container
- POSTGRES_INITDB_ARGS=--encoding=UTF-8 --lc-collate=C --lc-ctype=C
volumes:
- postgres_data:/var/lib/postgresql/data
healthcheck:
test: [ "CMD-SHELL", "pg_isready -U app_user -d users" ]
interval: 10s
timeout: 5s
retries: 5
start_period: 30s
restart: unless-stopped
deploy:
resources:
limits:
cpus: '1'
memory: 512M
reservations:
cpus: '0.25'
memory: 256M
networks:
- user-network
- app-network

volumes:
postgres_data:
name: user-service-postgres-data

networks:
user-network:
driver: bridge
app-network:
name: user-service-network
driver: bridge
34 changes: 27 additions & 7 deletions docker-compose.yml
Original file line number Diff line number Diff line change
@@ -1,6 +1,26 @@
# =============================================================================
# Docker Compose - Local Development (Database Only)
# =============================================================================
# Used by Spring Boot's Docker Compose integration when:
# spring.docker.compose.enabled=true (in application-local.yml)
#
# This file is auto-detected and managed by Spring Boot.
# The application runs on your host machine, only PostgreSQL runs in Docker.
#
# Usage:
# ./gradlew bootRun --args='--spring.profiles.active=local'
# (Spring Boot automatically starts/stops this compose file)
#
# Manual control:
# Start: docker compose up -d
# Stop: docker compose down
# Reset: docker compose down -v (removes data volume)
# =============================================================================

services:
postgres:
image: postgres:17.5-alpine
image: postgres:17-alpine
container_name: user-service-local-db
ports:
- "54321:5432"
environment:
Expand All @@ -9,12 +29,12 @@ services:
- POSTGRES_PASSWORD=test_user@pass01
volumes:
- postgres_data:/var/lib/postgresql/data
networks:
- user-network
healthcheck:
test: ["CMD-SHELL", "pg_isready -U test_user_rw -d users"]
interval: 10s
timeout: 5s
retries: 5

volumes:
postgres_data:

networks:
user-network:
driver: bridge
name: user-service-local-postgres-data
53 changes: 53 additions & 0 deletions src/main/resources/application-dev.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
# =============================================================================
# Development/Staging Profile
# =============================================================================
# Usage: Set SPRING_PROFILES_ACTIVE=dev
#
# Features:
# - Connects to external PostgreSQL (via environment variables)
# - Swagger UI enabled for API testing
# - Info-level logging
# - JWT secrets from environment variables (required)
#
# Required Environment Variables:
# - DATABASE_URL (e.g., jdbc:postgresql://dev-db.example.com:5432/users)
# - DATABASE_USERNAME
# - DATABASE_PASSWORD
# - JWT_ACCESS_SECRET (min 64 bytes for HS512)
# - JWT_REFRESH_SECRET (min 64 bytes for HS512)
# =============================================================================

spring:
# External database connection
datasource:
url: ${DATABASE_URL}
username: ${DATABASE_USERNAME}
password: ${DATABASE_PASSWORD}
driver-class-name: org.postgresql.Driver
hikari:
maximum-pool-size: 5
minimum-idle: 2
idle-timeout: 300000
max-lifetime: 1800000
connection-timeout: 30000

# gRPC reflection enabled for dev debugging
grpc:
server:
reflection:
enabled: true

# Enable Swagger UI for dev/staging
springdoc:
api-docs:
enabled: true
swagger-ui:
enabled: true

# Development logging (more verbose than prod)
logging:
level:
org.nkcoder: DEBUG
org.springframework.security: INFO
org.springframework.web: INFO
org.hibernate.SQL: DEBUG
Loading
Loading