Practical lab guide
MicroBank 2: build the local container profile
Package the real services with private databases, explicit health checks, and local ports.
On this page
Use one standalone Compose file
Work from the MicroBank repository root prepared in step 1. This profile is separate from upstream docker-compose.yml; always select it explicitly. It avoids inherited public port bindings, the old initializer, and the unused notifications database. Terraform will own messaging resources in step 3.
Create .study.env with these keys. Replace the LocalStack placeholder with a version or digest you have selected from its current supported images and checked for SNS, SQS, S3, persistence, and your CPU architecture. Follow the current LocalStack license/auth-token instructions; an old image in the source is not a current recommendation. Keep any token out of Git.
LOCALSTACK_IMAGE=REPLACE_WITH_REVIEWED_LOCALSTACK_IMAGE
LOCALSTACK_AUTH_TOKEN=
STUDY_TAG=dev
VITE_AUTH0_DOMAIN=
VITE_AUTH0_CLIENT_ID=
Protect the file with chmod 600 .study.env. Make an initially empty infra/study/runtime.env with mode 600; step 4 fills it before either API starts. The LocalStack token is distinct from the dummy AWS credentials used inside the emulator.
Save infra/study/compose.yaml:
services:
localstack:
image: ${LOCALSTACK_IMAGE:?Set a reviewed LocalStack image in .study.env}
environment:
SERVICES: sns,sqs,s3
LOCALSTACK_AUTH_TOKEN: ${LOCALSTACK_AUTH_TOKEN:-}
SQS_ENDPOINT_STRATEGY: path
PERSISTENCE: "1"
ports:
- "127.0.0.1:4566:4566"
volumes:
- localstack-data:/var/lib/localstack
healthcheck:
test: [CMD, curl, -fsS, http://localhost:4566/_localstack/health]
interval: 5s
timeout: 3s
retries: 30
mem_limit: 1536m
accounts-db:
image: postgres:15-bookworm
environment:
POSTGRES_DB: accounts_dev
POSTGRES_USER: postgres
POSTGRES_PASSWORD: study-only
volumes:
- accounts-data:/var/lib/postgresql/data
healthcheck:
test: [CMD-SHELL, "pg_isready -U postgres -d accounts_dev"]
interval: 5s
timeout: 3s
retries: 20
mem_limit: 512m
ledger-db:
image: postgres:15-bookworm
environment:
POSTGRES_DB: ledger_dev
POSTGRES_USER: postgres
POSTGRES_PASSWORD: study-only
volumes:
- ledger-data:/var/lib/postgresql/data
healthcheck:
test: [CMD-SHELL, "pg_isready -U postgres -d ledger_dev"]
interval: 5s
timeout: 3s
retries: 20
mem_limit: 512m
accounts:
image: microbank-study/accounts:${STUDY_TAG:-dev}
build:
context: ../../services/accounts
env_file: runtime.env
ports:
- "127.0.0.1:8000:8000"
depends_on:
accounts-db:
condition: service_healthy
localstack:
condition: service_healthy
healthcheck:
test: [CMD, python, -c, "import urllib.request; urllib.request.urlopen('http://127.0.0.1:8000/health', timeout=2)"]
interval: 5s
timeout: 3s
retries: 20
mem_limit: 512m
ledger:
image: microbank-study/ledger:${STUDY_TAG:-dev}
build:
context: ../..
dockerfile: infra/study/Dockerfile.ledger
env_file: runtime.env
environment:
SPRING_JPA_HIBERNATE_DDL_AUTO: update
JAVA_TOOL_OPTIONS: "-XX:MaxRAMPercentage=60"
ports:
- "127.0.0.1:8001:8001"
depends_on:
ledger-db:
condition: service_healthy
localstack:
condition: service_healthy
healthcheck:
test: [CMD, curl, -fsS, http://localhost:8001/v1/health]
interval: 5s
timeout: 3s
retries: 30
start_period: 30s
mem_limit: 768m
frontend:
profiles: [ui]
image: microbank-study/frontend:${STUDY_TAG:-dev}
build:
context: ../..
dockerfile: infra/study/Dockerfile.frontend
args:
VITE_API_BASE_URL: http://localhost:8000
VITE_AUTH0_DOMAIN: ${VITE_AUTH0_DOMAIN:-}
VITE_AUTH0_CLIENT_ID: ${VITE_AUTH0_CLIENT_ID:-}
ports:
- "127.0.0.1:8080:4173"
depends_on:
accounts:
condition: service_healthy
mem_limit: 384m
volumes:
accounts-data:
ledger-data:
localstack-data:
The fixed study-only password is a disposable fixture value on unpublished database ports. It is not a production secret. Memory limits are initial lab guardrails, not measured requirements; build processes need additional memory. Diagnose exit 137/OOM events and host pressure before changing limits. Do not start the optional frontend while resolving backend startup issues.
Build Ledger without the old single-stage image
Save infra/study/Dockerfile.ledger:
FROM maven:3.9-eclipse-temurin-17 AS build
WORKDIR /src
COPY services/ledger/pom.xml ./pom.xml
COPY services/ledger/src ./src
RUN mvn --batch-mode verify
FROM eclipse-temurin:17-jre-jammy
RUN apt-get update && apt-get install -y --no-install-recommends curl \
&& rm -rf /var/lib/apt/lists/*
WORKDIR /app
COPY --from=build /src/target/ledger-service-1.0.0.jar ./app.jar
USER 10001:10001
EXPOSE 8001
CMD ["java", "-jar", "app.jar"]
The inspected project has no Ledger test suite. mvn verify compiles/packages it and runs any tests you subsequently add; a successful build alone is not business-behavior coverage. The Compose override replaces create-drop with update for this local persistence exercise. Versioned database migrations are a later improvement; update is not a production migration strategy.
Make frontend build inputs explicit
Save infra/study/Dockerfile.frontend:
FROM node:22-bookworm-slim
WORKDIR /app
COPY frontend/microbank-frontend/package*.json ./
RUN npm ci
COPY frontend/microbank-frontend/ ./
ARG VITE_API_BASE_URL=http://localhost:8000
ARG VITE_AUTH0_DOMAIN
ARG VITE_AUTH0_CLIENT_ID
ENV VITE_API_BASE_URL=$VITE_API_BASE_URL
ENV VITE_AUTH0_DOMAIN=$VITE_AUTH0_DOMAIN
ENV VITE_AUTH0_CLIENT_ID=$VITE_AUTH0_CLIENT_ID
RUN npm run build
USER node
EXPOSE 4173
CMD ["npm", "run", "preview", "--", "--host", "0.0.0.0"]
This uses Vite preview only for local learning. Auth0 domain/client ID are public browser configuration, not client secrets. Put genuine SPA settings in .study.env to use login; configure callback http://localhost:8080/dashboard and the localhost origin/logout URL according to Auth0's instructions. Keep the existing authentication flow. A backend-only learner can complete the required API milestone without configuring the optional UI. VITE_* values are baked into the image: changing runtime environment values does not change compiled assets.
Save this as the root .dockerignore, and use the same exclusions in services/accounts/.dockerignore for that separate build context:
.git
.env*
**/.env*
.study.env
**/node_modules
**/target
**/__pycache__
infra/study/runtime.*
infra/study/terraform/.terraform
infra/study/terraform/*.tfstate*
infra/study/terraform/*.tfplan
artifacts
evidence
Docker ignores are relative to each build context. Record resolved base-image digests in your evidence; tags shown here are starting selections, not immutable pins.
Read the profile as a set of separate lifetimes
The two database services store application data in named volumes. Removing an application container need not erase those volumes. LocalStack holds the emulated API resources and needs its own persistence decision. The API images hold packaged code, while runtime.env supplies settings generated later. The optional frontend has a different build-time configuration path through Vite.
The health checks answer dependency readiness questions; they do not prove a deposit works. Database ports remain inside the Compose network, while deliberate host publications use loopback. In a container, the database service name identifies a peer; localhost would identify that container itself.
Trace the selected Compose file through compose.sh before running it. Its explicit project and file selection are what keep these commands attached to the learning profile. A command accidentally using the upstream Compose file can select different services and initialization behavior.
Validate and start infrastructure only
Save scripts/study/compose.sh to make the selected file and project name explicit:
#!/usr/bin/env bash
set -euo pipefail
root="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/../.." && pwd)"
exec docker compose --project-name microbank-study \
--env-file "$root/.study.env" -f "$root/infra/study/compose.yaml" "$@"
chmod +x scripts/study/compose.sh
./scripts/study/compose.sh config --quiet
./scripts/study/compose.sh up -d --wait localstack accounts-db ledger-db
./scripts/study/compose.sh ps
Do not start Accounts/Ledger until Terraform and Ansible complete. Do not mount upstream infra/localstack/init.sh: there must be one owner of the topic/queue lifecycle. Check the LocalStack startup logs if the token, image, or requested features are unavailable. Avoid printing a fully resolved Compose configuration containing credentials into public evidence.
Checkpoint and sources
Before continuing to Terraform, confirm that the three infrastructure services are healthy, the database ports are not published to the host, and the emulator is reachable through loopback. Sources: Compose startup order↗, profiles↗, LocalStack setup↗, queue URL strategy↗, Spring database initialization↗, Vite environment variables↗, Auth0 React setup↗.
Your notes and evidence
Record observations, questions, or links to your work. Keep credentials out of your notes.
Back up or restore this path
Progress and notes stay in this browser. A backup contains only this learning path.