diff --git a/docker/.env b/docker/.env
new file mode 100644
index 00000000..bc38ca40
--- /dev/null
+++ b/docker/.env
@@ -0,0 +1,24 @@
+# Project name for Docker Compose
+COMPOSE_PROJECT_NAME=folio-mod-notes
+
+# Module configuration
+MODULE_PORT=8081
+MODULE_REPLICAS=1
+DEBUG_PORT=5005
+
+# PostgreSQL configuration
+DB_HOST=postgres
+DB_PORT=5432
+DB_DATABASE=modules
+DB_USERNAME=folio_admin
+DB_PASSWORD=folio_admin
+
+# PgAdmin configuration
+PGADMIN_PORT=5050
+PGADMIN_DEFAULT_EMAIL=user@domain.com
+PGADMIN_DEFAULT_PASSWORD=admin
+
+# WireMock (Okapi mock) configuration
+WIREMOCK_PORT=9130
+OKAPI_URL=http://wiremock:8080
+
diff --git a/docker/README.md b/docker/README.md
new file mode 100644
index 00000000..c784caa7
--- /dev/null
+++ b/docker/README.md
@@ -0,0 +1,363 @@
+# 🐳 Docker Compose Setup for mod-notes
+
+Local development environment for mod-notes using Docker Compose.
+
+## 📋 Prerequisites
+
+- Docker and Docker Compose V2+
+- Java 21+ (for local development mode)
+- Maven 3.8+ (for building the module)
+
+## 🏗️ Architecture
+
+Two compose files provide flexible development workflows:
+
+- **`infra-docker-compose.yml`**: Infrastructure services only (PostgreSQL, pgAdmin, WireMock)
+- **`app-docker-compose.yml`**: Full stack including the module (uses `include` to incorporate infra services)
+
+## ⚙️ Configuration
+
+Configuration is managed via the `.env` file in this directory.
+
+### Key Environment Variables
+
+| Variable | Description | Default |
+|---------------------------|-------------------------------|------------------------|
+| `MODULE_REPLICAS` | Number of module instances | `1` |
+| `MODULE_PORT` | Module host port | `8081` |
+| `DEBUG_PORT` | Remote debugging port | `5005` |
+| `DB_HOST` | PostgreSQL hostname | `postgres` |
+| `DB_PORT` | PostgreSQL port | `5432` |
+| `DB_DATABASE` | Database name | `modules` |
+| `DB_USERNAME` | Database user | `folio_admin` |
+| `DB_PASSWORD` | Database password | `folio_admin` |
+| `PGADMIN_PORT` | PgAdmin port | `5050` |
+| `WIREMOCK_PORT` | WireMock (Okapi mock) port | `9130` |
+| `OKAPI_URL` | Okapi URL for the module | `http://wiremock:8080` |
+
+## 🚀 Services
+
+### PostgreSQL
+- **Purpose**: Primary database for module data
+- **Version**: PostgreSQL 16 Alpine
+- **Access**: localhost:5432 (configurable via `DB_PORT`)
+- **Credentials**: See `DB_USERNAME` and `DB_PASSWORD` in `.env`
+- **Database**: See `DB_DATABASE` in `.env`
+
+### pgAdmin
+- **Purpose**: Database administration interface
+- **Access**: http://localhost:5050 (configurable via `PGADMIN_PORT`)
+- **Login**: Use `PGADMIN_DEFAULT_EMAIL` and `PGADMIN_DEFAULT_PASSWORD` from `.env`
+
+### WireMock
+- **Purpose**: Mock Okapi and other FOLIO modules for local testing
+- **Access**: http://localhost:9130 (configurable via `WIREMOCK_PORT`)
+- **Mappings**: Located in `src/test/resources/mappings`
+
+## 📖 Usage
+
+> **Note**: All commands in this guide assume you are in the `docker/` directory. If you're at the project root, run `cd docker` first.
+
+### Starting the Environment
+
+```bash
+# Build the module first
+mvn -f ../pom.xml clean package -DskipTests
+
+# Start all services (infrastructure + module)
+docker compose -f app-docker-compose.yml up -d
+```
+
+```bash
+# Start only infrastructure services (for local development)
+docker compose -f infra-docker-compose.yml up -d
+```
+
+```bash
+# Start with build (if module code changed)
+docker compose -f app-docker-compose.yml up -d --build
+```
+
+```bash
+# Start specific service
+docker compose -f infra-docker-compose.yml up -d postgres
+```
+
+### Stopping the Environment
+
+```bash
+# Stop all services
+docker compose -f app-docker-compose.yml down
+```
+
+```bash
+# Stop infra services only
+docker compose -f infra-docker-compose.yml down
+```
+
+```bash
+# Stop and remove volumes (clean slate)
+docker compose -f app-docker-compose.yml down -v
+```
+
+### Viewing Logs
+
+```bash
+# All services
+docker compose -f app-docker-compose.yml logs
+```
+
+```bash
+# Specific service
+docker compose -f app-docker-compose.yml logs mod-notes
+```
+
+```bash
+# Follow logs in real-time
+docker compose -f app-docker-compose.yml logs -f mod-notes
+```
+
+```bash
+# Last 100 lines
+docker compose -f app-docker-compose.yml logs --tail=100 mod-notes
+```
+
+### Scaling the Module
+
+The module is configured with resource limits and deployment policies for production-like scaling:
+
+- **CPU Limits**: 1.0 CPU (max), 0.5 CPU (reserved)
+- **Memory Limits**: 512M (max), 256M (reserved)
+- **Restart Policy**: Automatic restart on failure
+- **Update Strategy**: Rolling updates with 1 instance at a time, 10s delay
+
+```bash
+# Scale to 3 instances
+docker compose -f app-docker-compose.yml up -d --scale mod-notes=3
+```
+
+```bash
+# Or modify MODULE_REPLICAS in .env and restart
+echo "MODULE_REPLICAS=3" >> .env
+docker compose -f app-docker-compose.yml up -d
+```
+
+### Cleanup and Reset
+
+```bash
+# Complete cleanup (stops containers, removes volumes)
+docker compose -f app-docker-compose.yml down -v
+```
+
+```bash
+# Remove all Docker resources
+docker compose -f app-docker-compose.yml down -v
+docker volume prune -f
+docker network prune -f
+```
+
+## 🔧 Development Workflows
+
+### Workflow 1: Full Docker Stack
+Run everything in Docker, including the module.
+
+```bash
+# Build the module
+mvn -f ../pom.xml clean package -DskipTests
+
+# Start all services
+docker compose -f app-docker-compose.yml up -d
+
+# View logs
+docker compose -f app-docker-compose.yml logs -f mod-notes
+```
+
+**Use Case**: Testing the full deployment, simulating production environment, scaling tests.
+
+### Workflow 2: Infrastructure Only + IDE
+Run infrastructure in Docker, develop the module in your IDE.
+
+```bash
+# Start infrastructure
+docker compose -f infra-docker-compose.yml up -d
+
+# Run module from IDE or command line
+mvn -f ../pom.xml spring-boot:run
+```
+
+**Use Case**: Active development with hot reload, debugging in IDE, faster iteration cycles.
+
+### Workflow 3: Spring Boot Docker Compose Integration
+Let Spring Boot manage Docker Compose automatically (recommended for rapid development).
+
+```bash
+# Run with dev profile (starts infrastructure automatically)
+mvn -f ../pom.xml spring-boot:run -Dspring-boot.run.profiles=dev
+```
+
+The `dev` profile is configured to:
+- Start services from `docker/infra-docker-compose.yml` automatically
+- Connect to services via localhost ports (PostgreSQL: 5432, WireMock: 9130)
+- Keep containers running after the application stops for faster subsequent startups
+
+**Use Case**: Quickest way to start development — no manual Docker commands needed.
+
+### Workflow 4: Spring Boot DevTools
+For rapid development with automatic restart on code changes.
+
+```bash
+# Start infrastructure
+docker compose -f infra-docker-compose.yml up -d
+
+# Run with devtools (automatic restart on code changes)
+mvn -f ../pom.xml spring-boot:run
+
+# Make code changes — application will automatically restart
+```
+
+**Use Case**: Continuous development with automatic reload, live code updates, rapid feedback loop.
+
+## 🛠️ Common Tasks
+
+### Building the Module
+
+```bash
+# Clean build (skip tests)
+mvn -f ../pom.xml clean package -DskipTests
+```
+
+```bash
+# Build with tests
+mvn -f ../pom.xml clean package
+```
+
+### Tenant Setup
+
+After starting the module, register a tenant by calling the `/_/tenant` API:
+
+```bash
+curl -X POST http://localhost:8081/_/tenant \
+ -H "Content-Type: application/json" \
+ -H "X-Okapi-Tenant: diku" \
+ -H "X-Okapi-Url: http://localhost:9130" \
+ -d '{"module_to": "mod-notes-8.0.0-SNAPSHOT", "parameters": [{"key": "loadReference", "value": "true"}]}'
+```
+
+> **Note**: Adjust `module_to` version to match the currently running module version.
+
+### Accessing Services
+
+```bash
+# PostgreSQL CLI
+docker compose -f infra-docker-compose.yml exec postgres psql -U folio_admin -d modules
+```
+
+```bash
+# View database tables
+docker compose -f infra-docker-compose.yml exec postgres psql -U folio_admin -d modules -c "\dt"
+```
+
+```bash
+# Check PostgreSQL health
+docker compose -f infra-docker-compose.yml exec postgres pg_isready -U folio_admin
+```
+
+### Rebuilding the Module
+
+```bash
+# Rebuild and restart the module
+mvn -f ../pom.xml clean package -DskipTests
+docker compose -f app-docker-compose.yml up -d --build mod-notes
+```
+
+```bash
+# Force rebuild without cache
+docker compose -f app-docker-compose.yml build --no-cache mod-notes
+docker compose -f app-docker-compose.yml up -d mod-notes
+```
+
+## 🐛 Troubleshooting
+
+### Port Conflicts
+
+If you encounter port conflicts, modify the ports in `.env`:
+
+```bash
+# Example: Change module port to 8082
+MODULE_PORT=8082
+```
+
+Then restart the services:
+
+```bash
+docker compose -f app-docker-compose.yml up -d
+```
+
+### Container Health Issues
+
+```bash
+# Check container status
+docker compose -f app-docker-compose.yml ps
+
+# Check specific container logs
+docker compose -f app-docker-compose.yml logs mod-notes
+
+# Restart a specific service
+docker compose -f app-docker-compose.yml restart mod-notes
+```
+
+### Database Connection Issues
+
+```bash
+# Verify PostgreSQL is running
+docker compose -f infra-docker-compose.yml ps postgres
+
+# Check PostgreSQL logs
+docker compose -f infra-docker-compose.yml logs postgres
+
+# Test database connection
+docker compose -f infra-docker-compose.yml exec postgres psql -U folio_admin -d modules -c "SELECT 1"
+```
+
+**`FATAL: database "modules" does not exist`** — PostgreSQL only creates the database defined in `POSTGRES_DB` on the very first startup with an empty data directory. If the `postgres-data` volume already existed from a previous run (with different settings), the init is skipped. Fix by recreating the volume:
+
+```bash
+docker compose -f infra-docker-compose.yml stop postgres pgadmin
+docker compose -f infra-docker-compose.yml rm -f postgres pgadmin
+docker volume rm folio-mod-notes_postgres-data
+docker compose -f infra-docker-compose.yml up -d postgres pgadmin
+```
+
+### Clean Start
+
+If you need to completely reset the environment:
+
+```bash
+# Stop and remove everything
+docker compose -f app-docker-compose.yml down -v
+
+# Remove any orphaned containers
+docker container prune -f
+
+# Remove unused networks
+docker network prune -f
+
+# Start fresh
+mvn -f ../pom.xml clean package -DskipTests
+docker compose -f app-docker-compose.yml up -d --build
+```
+
+## 📚 Additional Resources
+
+- [Docker Compose Documentation](https://docs.docker.com/compose/)
+- [Spring Boot Docker Compose Support](https://docs.spring.io/spring-boot/docs/current/reference/html/features.html#features.docker-compose)
+- [mod-notes Documentation](../README.md)
+
+## 💡 Tips
+
+- Use **Workflow 3** (Spring Boot Docker Compose) for the fastest development experience
+- Keep infrastructure running between development sessions to save startup time
+- Use **Workflow 1** (Full Docker Stack) when testing deployment or scaling scenarios
+- Use `docker compose -f infra-docker-compose.yml logs -f` to monitor all infrastructure services
+- pgAdmin provides a helpful web interface for inspecting the database at http://localhost:5050
+
diff --git a/docker/app-docker-compose.yml b/docker/app-docker-compose.yml
new file mode 100644
index 00000000..3b9d6d60
--- /dev/null
+++ b/docker/app-docker-compose.yml
@@ -0,0 +1,43 @@
+# Full application stack for mod-notes (infrastructure + module)
+# This includes all infrastructure services and the mod-notes module itself
+
+include:
+ - infra-docker-compose.yml
+
+services:
+ mod-notes:
+ image: dev.folio/mod-notes:latest
+ build:
+ context: ../
+ dockerfile: Dockerfile
+ ports:
+ - "${MODULE_PORT}:8081"
+ - "${DEBUG_PORT}:5005"
+ environment:
+ DB_HOST: ${DB_HOST}
+ DB_PORT: ${DB_PORT}
+ DB_DATABASE: ${DB_DATABASE}
+ DB_USERNAME: ${DB_USERNAME}
+ DB_PASSWORD: ${DB_PASSWORD}
+ OKAPI_URL: ${OKAPI_URL}
+ JAVA_OPTIONS: "-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=*:5005"
+ networks:
+ - mod-notes-local
+ depends_on:
+ - postgres
+ - wiremock
+ deploy:
+ replicas: ${MODULE_REPLICAS:-1}
+ restart_policy:
+ condition: on-failure
+ update_config:
+ parallelism: 1
+ delay: 10s
+ resources:
+ limits:
+ cpus: "1.0"
+ memory: "512M"
+ reservations:
+ cpus: "0.5"
+ memory: "256M"
+
diff --git a/docker/infra-docker-compose.yml b/docker/infra-docker-compose.yml
new file mode 100644
index 00000000..005e7f10
--- /dev/null
+++ b/docker/infra-docker-compose.yml
@@ -0,0 +1,55 @@
+# Infrastructure services for mod-notes
+# This file contains all infrastructure dependencies (database, pgAdmin, WireMock/Okapi mock)
+# It can be used standalone for local development or included in app-docker-compose.yml
+
+services:
+ postgres:
+ image: postgres:16-alpine
+ ports:
+ - ${DB_PORT}:5432
+ environment:
+ POSTGRES_USER: ${DB_USERNAME}
+ POSTGRES_PASSWORD: ${DB_PASSWORD}
+ POSTGRES_DB: ${DB_DATABASE}
+ volumes:
+ - postgres-data:/var/lib/postgresql/data
+ networks:
+ - mod-notes-local
+
+ pgadmin:
+ image: dpage/pgadmin4:latest
+ ports:
+ - ${PGADMIN_PORT}:80
+ environment:
+ PGADMIN_DEFAULT_EMAIL: ${PGADMIN_DEFAULT_EMAIL}
+ PGADMIN_DEFAULT_PASSWORD: ${PGADMIN_DEFAULT_PASSWORD}
+ PGADMIN_CONFIG_SERVER_MODE: 'False'
+ PGADMIN_CONFIG_MASTER_PASSWORD_REQUIRED: 'False'
+ PGADMIN_SERVER_JSON_FILE: '/pgadmin4/servers.json'
+ PGADMIN_REPLACE_SERVERS_ON_STARTUP: 'True'
+ volumes:
+ - pgadmin-data:/var/lib/pgadmin
+ - ./pgadmin-servers.json:/pgadmin4/servers.json:ro
+ depends_on:
+ - postgres
+ networks:
+ - mod-notes-local
+
+ wiremock:
+ image: wiremock/wiremock:latest
+ ports:
+ - ${WIREMOCK_PORT}:8080
+ volumes:
+ - ../src/test/resources/mappings:/home/wiremock/mappings
+ entrypoint: ["/docker-entrypoint.sh", "--global-response-templating", "--disable-gzip", "--verbose"]
+ networks:
+ - mod-notes-local
+
+networks:
+ mod-notes-local:
+ driver: bridge
+
+volumes:
+ postgres-data:
+ pgadmin-data:
+
diff --git a/docker/pgadmin-servers.json b/docker/pgadmin-servers.json
new file mode 100644
index 00000000..b49fc3da
--- /dev/null
+++ b/docker/pgadmin-servers.json
@@ -0,0 +1,14 @@
+{
+ "Servers": {
+ "1": {
+ "Name": "mod-notes (local)",
+ "Group": "Servers",
+ "Host": "postgres",
+ "Port": 5432,
+ "MaintenanceDB": "modules",
+ "Username": "folio_admin",
+ "SSLMode": "prefer"
+ }
+ }
+}
+
diff --git a/pom.xml b/pom.xml
index f5529986..2af16b2c 100644
--- a/pom.xml
+++ b/pom.xml
@@ -110,6 +110,20 @@
${jsoup.version}
+
+ org.springframework.boot
+ spring-boot-docker-compose
+ runtime
+ true
+
+
+
+ org.springframework.boot
+ spring-boot-devtools
+ runtime
+ true
+
+
org.springframework.boot
diff --git a/src/main/resources/application-dev.yaml b/src/main/resources/application-dev.yaml
new file mode 100644
index 00000000..fa49cadd
--- /dev/null
+++ b/src/main/resources/application-dev.yaml
@@ -0,0 +1,121 @@
+# Development profile for local development with Spring Boot Docker Compose support
+# Activate this profile with: --spring.profiles.active=dev or SPRING_PROFILES_ACTIVE=dev
+
+spring:
+ application:
+ name: mod-notes
+ threads:
+ virtual:
+ enabled: true
+ datasource:
+ url: jdbc:postgresql://${DB_HOST:localhost}:${DB_PORT:5432}/${DB_DATABASE:modules}
+ username: ${DB_USERNAME:folio_admin}
+ password: ${DB_PASSWORD:folio_admin}
+ hikari:
+ connectionTimeout: ${DB_CONNECTION_TIMEOUT:30000}
+ idleTimeout: ${DB_IDLE_TIMEOUT:600000}
+ keepaliveTime: ${DB_KEEPALIVE_TIME:0}
+ maxLifetime: ${DB_MAX_LIFETIME:1800000}
+ validationTimeout: ${DB_VALIDATION_TIMEOUT:5000}
+ maximumPoolSize: ${DB_MAXPOOLSIZE:10}
+ minimumIdle: ${DB_MINIMUM_IDLE:10}
+ initializationFailTimeout: ${DB_INITIALIZATION_FAIL_TIMEOUT:30000}
+ leakDetectionThreshold: ${DB_LEAK_DETECTION_THRESHOLD:60000}
+ liquibase:
+ changeLog: classpath:db/changelog/changelog-master.xml
+ enabled: true
+ jpa:
+ open-in-view: true
+ hibernate:
+ ddl-auto: none
+ properties:
+ hibernate:
+ dialect: org.hibernate.dialect.PostgreSQLDialect
+ format_sql: true
+ show-sql: false
+ jackson:
+ default-property-inclusion: NON_NULL
+ deserialization:
+ fail-on-unknown-properties: false
+ accept-single-value-as-array: true
+ cache:
+ type: caffeine
+ sql:
+ init:
+ continue-on-error: true
+ docker:
+ compose:
+ enabled: true
+ file: ./docker/infra-docker-compose.yml
+
+folio:
+ okapi-url: ${OKAPI_URL:http://localhost:9130}
+ exchange:
+ enabled: true
+ tenant:
+ validation:
+ enabled: true
+ logging:
+ request:
+ enabled: true
+ level: basic
+ exchange:
+ enabled: true
+ level: basic
+ notes:
+ cache:
+ configs:
+ users-by-id:
+ initialCapacity: 20
+ maximumSize: 100
+ expireAfterWrite: 60
+ types:
+ defaults:
+ name: General note
+ limit: ${NOTES_TYPES_DEFAULTS_LIMIT:25}
+ content:
+ allowed:
+ tags:
+ - p
+ - strong
+ - em
+ - a
+ - u
+ - ol
+ - ul
+ - li
+ - h1
+ - h2
+ - h3
+ - br
+ attributes:
+ all:
+ - class
+ a:
+ - href
+ - rel
+ - target
+ response:
+ limit: ${MAX_RECORDS_COUNT:1000}
+
+management:
+ endpoints:
+ web:
+ exposure:
+ include: info,health,env,httptrace,loggers
+ base-path: /admin
+ health:
+ defaults:
+ enabled: false
+ readinessstate:
+ enabled: true
+ endpoint:
+ loggers:
+ access: unrestricted
+
+server:
+ port: 8081
+
+logging:
+ level:
+ org.folio.spring.filter.IncomingRequestLoggingFilter: DEBUG