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
15 changes: 15 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# Build output and caches
target/
venv/

# VCS, IDE, agent state
.git/
.idea/
.claude/

# Secrets: the FIFO/file is provided at runtime, never baked into the image
.env

# Docs, design assets, CI: not needed inside the image
docs/
.github/
10 changes: 10 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -118,3 +118,13 @@ MONGO_PORT=27018
# See: https://docs.spring.io/spring-boot/3.5/how-to/properties-and-configuration.html#howto.properties-and-configuration.set-active-spring-profiles
#
SPRING_PROFILES_ACTIVE=dev

# ~~~ Production SMTP relay (prod profile only; Mailpit covers dev) ~~~~~~~~~~

# Hostname of the SMTP relay that delivers transactional emails
# (password reset, email verification).
#SMTP_HOST=
# Submission port of the relay (587 = STARTTLS, the prod profile default).
#SMTP_PORT=587
#SMTP_USERNAME=
#SMTP_PASSWORD=
34 changes: 34 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
# learn-dev application image (multi-stage).
# Build: docker build -t learn-dev .
# Run: docker run --rm -p 8080:8080 --env-file <prod env file> learn-dev
# The image is self-contained and is NOT published anywhere; no Maven
# artifact is deployed to any public repository either (the pom has no
# distributionManagement).

# ---- Stage 1: build the executable jar ------------------------------------
FROM maven:3.9-eclipse-temurin-21 AS build
WORKDIR /workspace

# Warm the dependency cache first so code changes do not re-download the world.
COPY pom.xml .
RUN mvn -q -B dependency:go-offline

COPY src ./src
RUN mvn -q -B -DskipTests package

# ---- Stage 2: minimal runtime ----------------------------------------------
FROM eclipse-temurin:21-jre
WORKDIR /app

# Run as a non-root user.
RUN useradd --system --home /app learndev
USER learndev

COPY --from=build /workspace/target/*.jar app.jar

# Configuration comes from environment variables (see .env.example):
# POSTGRES_HOST/PORT, LEARNDEV_DB_*, SMTP_*, ...
ENV SPRING_PROFILES_ACTIVE=prod
EXPOSE 8080

ENTRYPOINT ["java", "-jar", "app.jar"]
23 changes: 23 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -249,6 +249,29 @@ For each service (`postgres`, `mongo`, and `mailpit`):
> The containers run inside that hidden *VM*, not directly on macOS/Windows.


## Package for production (Docker)

The [Dockerfile](Dockerfile) builds a self-contained application image in
two stages (Maven build, then a minimal JRE runtime running as a non-root
user, `prod` profile active by default):

```shell
docker build -t learn-dev .
docker run --rm -p 8080:8080 --env-file .env.prod learn-dev
```

The runtime configuration comes entirely from environment variables (see
[.env.example](.env.example)): the PostgreSQL coordinates and credentials,
and the SMTP relay (`SMTP_*`) used by the password-reset and
email-verification emails. The prod profile assumes a TLS-terminating
reverse proxy in front of the app: it honors `X-Forwarded-*` headers and
marks the session cookie `Secure`.

> [!NOTE]
> Packaging is local only: the image is not pushed to a registry, and no
> Maven artifact is deployed to any public repository (the `pom.xml`
> deliberately has no `distributionManagement`).

## Stop the Application

This command stops all the application services containers
Expand Down
45 changes: 32 additions & 13 deletions src/main/resources/application-prod.yaml
Original file line number Diff line number Diff line change
@@ -1,14 +1,16 @@
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
# application-prod.yaml
#
# Application configuration file for the dev profile.
# Application configuration for the prod profile.
# Properties defined in this file override the ones defined
# in application.yaml.
#
# To activate the dev profile:
# - Add SPRING_PROFILES_ACTIVE=dev to your .env file
# - or set the environment variable SPRING_PROFILES_ACTIVE=dev
# - or run: mvn spring-boot:run -Dspring-boot.run.profiles=dev
# To activate the prod profile:
# - set the environment variable SPRING_PROFILES_ACTIVE=prod
# (the container image in the Dockerfile does this by default)
#
# The app expects TLS to be terminated by a reverse proxy in front
# of it; forwarded headers tell Spring the original scheme/host.
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
spring:

Expand All @@ -18,19 +20,36 @@ spring:
jpa:
show-sql: false

# Generate DDL and write to file
# properties:
# javax.persistence.schema-generation.scripts:
# action: create
# create-target: schema.sql
# create-source: metadata

# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
# Set the execution context for Liquibase changeset
# Set the execution context for Liquibase changesets
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
liquibase:
contexts: prod

# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
# Real SMTP relay (Mailpit is dev-only)
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
mail:
host: ${SMTP_HOST}
port: ${SMTP_PORT:587}
username: ${SMTP_USERNAME:}
password: ${SMTP_PASSWORD:}
properties:
mail.smtp.auth: true
mail.smtp.starttls.enable: true


server:
# Behind a TLS-terminating reverse proxy: honor X-Forwarded-* headers so
# generated links (password reset, email verification) use https.
forward-headers-strategy: framework
servlet:
session:
cookie:
# The proxy serves HTTPS only, so the session cookie never travels
# over plain HTTP.
secure: true


# ~~~~~~~~~~~~~~~~~~
# Logging Level
Expand Down