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.

  • Docker
  • PostgreSQL
Documentation reviewed2026-10-01 · 4 min read · lab time varies
  1. Why
  2. Predict
  3. Try
  4. Inspect
  5. Explain
  6. Save evidence

Predict before you try

Can Accounts acceptance alone prove this saved deposit reached Ledger exactly once?

Try it: Build Ledger without the old single-stage image →
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.

For the first run, use the downloadable starter from step 1. It supplies the exact files shown below and pinned Floci/PostgreSQL image digests. python3 scripts/study/starter.py prepare creates .study.env without overwriting an existing file. The Floci runtime uses dummy AWS credentials and requires no LocalStack token.

The starter uses a fixed project name, learnwithsk-microbank-v1, and host ports 24666, 28080, 28081. Run the prerequisite checker first; an occupied port is a reason to inspect its owner, not stop an unrelated service. Floci uses its image-provided health check and persistent storage at /app/data. No Docker socket is mounted.

Save infra/study/compose.yaml:

yaml
services:
  floci:
    image: ${FLOCI_IMAGE:?Run the starter preparation first}
    environment:
      FLOCI_STORAGE_MODE: persistent
      FLOCI_STORAGE_PERSISTENT_PATH: /app/data
      FLOCI_DEFAULT_REGION: ca-central-1
      FLOCI_HOSTNAME: floci
      FLOCI_BASE_URL: http://floci:4566
    ports:
      - "127.0.0.1:24666:4566"
    volumes:
      - floci-data:/app/data
    healthcheck:
      interval: 5s
      timeout: 3s
      retries: 30
    mem_limit: 1536m

  accounts-db:
    image: ${POSTGRES_IMAGE:?Run the starter preparation first}
    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_IMAGE:?Run the starter preparation first}
    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:-starter-v1}
    build:
      context: ../../services/accounts
    env_file: runtime.env
    ports:
      - "127.0.0.1:28080:8000"
    depends_on:
      accounts-db:
        condition: service_healthy
      floci:
        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:-starter-v1}
    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:28081:8001"
    depends_on:
      ledger-db:
        condition: service_healthy
      floci:
        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

volumes:
  accounts-data:
  ledger-data:
  floci-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.

Optional frontend scope

The downloadable profile verifies the two backend APIs. The existing Auth0 frontend is a later extension with its own build-time Vite inputs and callback/CORS settings. It is not declared in this backend-only Compose file. Its sample notifications and initial balance display do not establish the Ledger result.

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. Floci holds the emulated API resources; this profile explicitly enables persistent storage. 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 learnwithsk-microbank-v1 \
  --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 floci 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 Floci startup logs and pinned image identity if readiness fails. 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↗, Floci setup↗, queue URL strategy↗, Spring database initialization↗, Vite environment variables↗, Auth0 React setup↗.

Prove what you learned

Inspect the result

Retain selected source/profile, case identity, expected balance and matching-entry count.

Explain it in your own words

Explain why your observation supports the stated outcome, and name one thing it does not prove.

Try a small variation

Repeat the same check after the documented restart or recovery; preserve the original case instead of creating a replacement.

Your evidence should show

  • Record the context/revision or label the work as a paper or prepared fixture.
  • Compare the prediction with the actual result from “Build Ledger without the old single-stage image”.
  • Retain selected source/profile, case identity, expected balance and matching-entry count.
  • Explain the transfer challenge and retain the same verification question.
Hint

Follow the case identifier across the outbox, message route and Ledger result.

Answer guide: compare after predicting

No. Repeat the idempotency key, then verify exactly one matching Ledger entry and the expected balance.

Compare this with your actual observation. A paper answer is useful reasoning practice; record execution separately.

Recall private networking, persistent volumes and bounded Python checks. Revisit the previous lesson.

Save the prediction, result and explanation ↓

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.