Skip to content
← DevOps foundations

Practical lab guide

MicroBank 2: build the local container profile

Package the real services with private databases, explicit health checks, and local ports.

Documentation reviewed2026-10-01 · 5 min read · lab time varies
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.

dotenv
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:

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:

dockerfile
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:

dockerfile
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:

text
.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:

bash
#!/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" "$@"
bash
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.

Loading saved progress…

Back up or restore this path

Progress and notes stay in this browser. A backup contains only this learning path.