DEV Community

Majedul Islam
Majedul Islam

Posted on

Polyglot Dockerization: Java + Python + Vue in Local and Production

Polyglot Dockerization: Java + Python + Vue in Local and Production

  • Why these services: A polyglot stack where Spring Boot powers core business workflows, FastAPI powers AI/RAG, and Vue powers the frontend.
  • Why dockerization matters: Containers keep Java, Python, and Node toolchains consistent everywhere, eliminating cross-language environment drift.
  • Two goals: (1) frictionless local development with one command, (2) production-grade images and configuration for reliable deploys.

Keep secrets and configuration in .env files. Compose reads values like POSTGRES_PASSWORD, JWT_SECRET, GOOGLE_API_KEY, etc., from a local .env placed next to docker-compose.yml.

Local Dockerization with Docker Compose

  • Spring Boot service

Dockerfile (multi-stage: build with Maven, run on JRE):

# ---------- Build stage ---------- FROM maven:3.9-eclipse-temurin-21 AS builder WORKDIR /workspace/legalconnect # Copy Maven wrapper and pom.xml first for better caching COPY legalconnect/mvnw legalconnect/mvnw.cmd ./ COPY legalconnect/.mvn ./.mvn COPY legalconnect/pom.xml ./ # Download dependencies RUN ./mvnw dependency:go-offline -B # Copy source code and build COPY legalconnect/src ./src RUN ./mvnw clean package -DskipTests -B # ---------- Runtime stage ---------- FROM eclipse-temurin:21-jre-alpine WORKDIR /app # Non-root user + logs dir RUN addgroup -g 1001 -S spring && adduser -S spring -u 1001 \  && mkdir -p /app/logs && chown -R spring:spring /app # Copy fat jar COPY --from=builder /workspace/legalconnect/target/legalconnect-0.0.1-SNAPSHOT.jar /app/app.jar RUN chown spring:spring /app/app.jar USER spring ENV JAVA_OPTS="-Xms512m -Xmx1024m -XX:+UseG1GC -XX:+UseContainerSupport" ENV SERVER_PORT=8080 EXPOSE 8080 ENTRYPOINT ["sh", "-c", "java $JAVA_OPTS -jar /app/app.jar"] 
Enter fullscreen mode Exit fullscreen mode
  • FastAPI service

Dockerfile (Python slim base, pinned requirements, uvicorn):

FROM python:3.12-slim WORKDIR /app COPY requirements.txt . RUN pip install --no-cache-dir --upgrade pip \  && pip install --no-cache-dir -r requirements.txt COPY . . EXPOSE 8000 CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"] 
Enter fullscreen mode Exit fullscreen mode
  • Frontend (Vue) service

Dockerfile (build with Node 22, serve static with serve):

# ---- Build stage ---- FROM node:22-alpine AS builder WORKDIR /app # Install deps first (better layer caching) COPY package*.json ./ RUN npm ci # Copy source COPY . . # Accept build-time envs for Vite ARG VITE_API_BASE_URL ARG VITE_AI_CHAT_BASE_URL ARG VITE_JAAS_URL ARG VITE_JITSI_APP_ID ENV VITE_API_BASE_URL=${VITE_API_BASE_URL} ENV VITE_AI_CHAT_BASE_URL=${VITE_AI_CHAT_BASE_URL} ENV VITE_JAAS_URL=${VITE_JAAS_URL} ENV VITE_JITSI_APP_ID=${VITE_JITSI_APP_ID} # Build RUN npm run build # ---- Runtime stage (serve) ---- FROM node:22-alpine WORKDIR /app ENV NODE_ENV=production RUN npm i -g serve@14 COPY --from=builder /app/dist /app # Use port 5173 to match development server ENV PORT=5173 EXPOSE 5173 # Bind to 0.0.0.0 and use port 5173 CMD ["sh","-c","serve -s /app -l tcp://0.0.0.0:${PORT}"] 
Enter fullscreen mode Exit fullscreen mode
  • docker-compose.yml (all services)

One Compose file orchestrates everything locally: Postgres, Redis, Elasticsearch, Spring Boot, FastAPI, and the frontend. Services depend on each other with health checks. Environment variables are injected from .env.

services: postgres: image: postgres:17.5 environment: - POSTGRES_DB=${POSTGRES_DB:-legalconnect} - POSTGRES_USER=${POSTGRES_USER:-root} - POSTGRES_PASSWORD=${POSTGRES_PASSWORD} ports: ["5432:5432"] volumes: - postgres_data:/var/lib/postgresql/data - ./backend/legalconnect/src/main/resources/quartz_tables.sql:/docker-entrypoint-initdb.d/10_quartz.sql:ro healthcheck: test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-root} -d ${POSTGRES_DB:-legalconnect}"] redis: image: redis:7 command: ["redis-server", "--appendonly", "yes"] ports: ["6379:6379"] healthcheck: test: ["CMD", "redis-cli", "ping"] elasticsearch: image: docker.elastic.co/elasticsearch/elasticsearch:9.1.1 environment: - discovery.type=single-node - xpack.security.enabled=false - ES_JAVA_OPTS=-Xms512m -Xmx512m ports: ["9200:9200"] backend: build: { context: ./backend, dockerfile: Dockerfile } environment: - SPRING_DATASOURCE_URL=jdbc:postgresql://postgres:5432/${POSTGRES_DB:-legalconnect} - SPRING_DATASOURCE_USERNAME=${POSTGRES_USER:-root} - SPRING_DATASOURCE_PASSWORD=${POSTGRES_PASSWORD} - SPRING_DATA_REDIS_HOST=redis - SPRING_DATA_REDIS_PORT=6379 - SPRING_ELASTICSEARCH_URIS=http://elasticsearch:9200 - SPRING_CUSTOM_SECURITY_JWTSECRET=${JWT_SECRET} - FRONTEND_URL=${FRONTEND_URL:-http://localhost:5173} ports: ["8080:8080"] depends_on: postgres: { condition: service_healthy } redis: { condition: service_healthy } elasticsearch: { condition: service_healthy } backend-ai: build: { context: ./backend-ai, dockerfile: Dockerfile } environment: - DATABASE_URL=postgresql://${POSTGRES_USER:-root}:${POSTGRES_PASSWORD}@postgres:5432/${POSTGRES_DB:-legalconnect} - REDIS_URL=redis://redis:6379/0 - QDRANT_URL=${QDRANT_URL} - QDRANT_API_KEY=${QDRANT_API_KEY} - QDRANT_COLLECTION_NAME=${QDRANT_COLLECTION_NAME} - GOOGLE_API_KEY=${GOOGLE_API_KEY} - JWT_SECRET_KEY=${JWT_SECRET} ports: ["8000:8000"] depends_on: postgres: { condition: service_healthy } redis: { condition: service_healthy } frontend: build: context: ./frontend dockerfile: Dockerfile args: - VITE_API_BASE_URL=${VITE_API_BASE_URL:-http://localhost:8080/v1} - VITE_AI_CHAT_BASE_URL=${VITE_AI_CHAT_BASE_URL:-http://localhost:8000/api/v1} ports: ["5173:5173"] volumes: postgres_data: {} 
Enter fullscreen mode Exit fullscreen mode
  • What this compose does

    • Brings up stateful services (Postgres, Redis, Elasticsearch) first, then app services.
    • Uses container DNS for internal networking (postgres, redis, elasticsearch).
    • Exposes only required ports to the host, keeping service internals private.
    • Injects configuration via .env and built-in defaults for quick start.
  • Benefits: One command (docker compose up -d) to bring the entire stack up; reproducible environments; isolated dependencies; easy resets via volumes.

  • Challenges: Service-to-service networking (use container names like postgres, redis); managing env vars across services (centralize in .env); ensuring startup order (use depends_on + health checks); volume mounts for logs/data; port collisions.

Run Only One Backend (single-service Compose files)

Sometimes you need to run just one backend service for focused development.

  • Spring Boot only (backend/docker-compose.yml):
authors-note: run only Spring Boot with Postgres/Redis locally services: db: image: postgres:17.5 environment: - POSTGRES_DB=${POSTGRES_DB:-legalconnect} - POSTGRES_USER=${POSTGRES_USER:-root} - POSTGRES_PASSWORD=${POSTGRES_PASSWORD} ports: ["5432:5432"] redis: image: redis:7 ports: ["6379:6379"] backend: image: maven:3.9-eclipse-temurin-21 working_dir: /workspace/backend/legalconnect command: ./mvnw spring-boot:run environment: - SPRING_DATASOURCE_URL=jdbc:postgresql://db:5432/${POSTGRES_DB:-legalconnect} - SPRING_DATASOURCE_USERNAME=${POSTGRES_USER:-root} - SPRING_DATASOURCE_PASSWORD=${POSTGRES_PASSWORD} - SPRING_DATA_REDIS_HOST=redis - SPRING_DATA_REDIS_PORT=6379 volumes: - ./legalconnect:/workspace/backend/legalconnect - ~/.m2:/root/.m2 ports: ["8080:8080"] depends_on: db: { condition: service_started } redis: { condition: service_started } 
Enter fullscreen mode Exit fullscreen mode
  • What this compose does

    • Runs Spring Boot directly via Maven wrapper inside the container for fast feedback.
    • Mounts your project folder and local Maven cache for quick rebuilds.
    • Provides local Postgres + Redis with simple default credentials.
  • FastAPI only (backend-ai/docker-compose.yml):

authors-note: run only FastAPI with Postgres/Redis locally services: db: image: postgres:17.5 environment: - POSTGRES_DB=${POSTGRES_DB:-legal_connect_db} - POSTGRES_USER=${POSTGRES_USER:-legal} - POSTGRES_PASSWORD=${POSTGRES_PASSWORD} ports: ["5432:5432"] redis: image: redis:7 ports: ["6379:6379"] fastapi-app: build: { context: ., dockerfile: Dockerfile } environment: - DATABASE_URL=postgresql://${POSTGRES_USER:-legal}:${POSTGRES_PASSWORD}@db:5432/${POSTGRES_DB:-legal_connect_db} - REDIS_URL=redis://redis:6379/0 ports: ["8000:8000"] depends_on: db: { condition: service_started } redis: { condition: service_started } 
Enter fullscreen mode Exit fullscreen mode
  • What this compose does
    • Builds your FastAPI app image and runs it with DB/Redis dependencies.
    • Uses the standard DATABASE_URL/REDIS_URL patterns for twelve-factor configuration.
    • Exposes only the FastAPI port to your host for API testing.

How to run locally

1) Prepare environment

  • Keep you environment variables and secrets (DB password, JWT secrets, API keys) in a .env file.
  • Ensure no local services are occupying ports: 5432, 6379, 9200, 8080, 8000, 5173. Stop them or free ports.

2) Run the full stack

  • Build and start: docker compose up -d --build
  • Check health: docker compose ps and docker compose logs -f backend backend-ai frontend
  • App URLs: backend http://localhost:8080, AI http://localhost:8000, frontend http://localhost:5173

3) Run only one backend

  • Spring Boot only: docker compose -f backend/docker-compose.yml up -d
  • FastAPI only: docker compose -f backend-ai/docker-compose.yml up -d

4) Tear down and reset

  • Stop: docker compose down
  • Stop + remove volumes (DB reset): docker compose down -v

Production Dockerization

  • Spring Boot optimized Dockerfile (multi-stage, smaller image)
# Build stage FROM maven:3.9-eclipse-temurin-21 AS builder WORKDIR /workspace/legalconnect COPY legalconnect/mvnw legalconnect/mvnw.cmd ./ COPY legalconnect/.mvn ./.mvn COPY legalconnect/pom.xml ./ RUN ./mvnw dependency:go-offline -B COPY legalconnect/src ./src RUN ./mvnw clean package -DskipTests -B # Runtime stage FROM eclipse-temurin:21-jre-alpine WORKDIR /app RUN addgroup -g 1001 -S spring && adduser -S spring -u 1001 \  && mkdir -p /app/logs && chown -R spring:spring /app COPY --from=builder /workspace/legalconnect/target/legalconnect-0.0.1-SNAPSHOT.jar /app/app.jar USER spring ENV SPRING_PROFILES_ACTIVE=prod ENV JAVA_OPTS="-XX:+UseContainerSupport -XX:MaxRAMPercentage=75.0 -XX:+UseG1GC -XX:+UseStringDeduplication" ENV PORT=8080 EXPOSE 8080 ENTRYPOINT ["sh", "-c", "java $JAVA_OPTS -Dserver.port=$PORT -jar /app/app.jar"] 
Enter fullscreen mode Exit fullscreen mode
  • FastAPI optimized Dockerfile (lightweight base, non-root, healthcheck)
FROM python:3.12-slim WORKDIR /app RUN apt-get update && apt-get install -y gcc g++ \  && rm -rf /var/lib/apt/lists/* COPY requirements.txt . RUN pip install --no-cache-dir --upgrade pip \  && pip install --no-cache-dir -r requirements.txt COPY . . RUN groupadd -r appuser && useradd -r -g appuser appuser \  && mkdir -p /app/logs /app/bdcode_json \  && chown -R appuser:appuser /app USER appuser ENV PYTHONPATH=/app ENV PYTHONUNBUFFERED=1 ENV PYTHONDONTWRITEBYTECODE=1 ENV PORT=8000 EXPOSE 8000 HEALTHCHECK --interval=30s --timeout=10s --start-period=60s --retries=3 \ CMD python -c "import requests; requests.get('http://localhost:$PORT/health')" || exit 1 CMD ["sh", "-c", "uvicorn main:app --host 0.0.0.0 --port $PORT --workers 1"] 
Enter fullscreen mode Exit fullscreen mode
  • Config differences vs local: In production, deploy images directly (Cloud Run/Kubernetes). Prefer managed services: Cloud SQL (Postgres), Memorystore (Redis), Elastic Cloud. Service discovery and autoscaling are handled by the platform.
  • Handling env vars + secrets: Use Secret Manager/Key Vault and runtime env vars. Keep .env for local only; mirror variable names across environments for parity.

Comparing Local vs Prod Setup

Service Local Production
Spring Boot Compose + Postgres/Redis/Elasticsearch Multi-stage image, managed Postgres (Cloud SQL), managed Redis (Memorystore), Elastic Cloud
FastAPI Compose + Postgres + Redis Lightweight image, managed Postgres/Redis, scale-to-zero via Cloud Run
Frontend Compose-built image (Vite args) Static hosting or container image served behind CDN
Config .env + docker compose Secret manager + platform env vars
Networking Container DNS names (postgres, redis) Platform service URLs, VPC connectors if needed

Potential Issues to Watch For

  • Port conflicts: Stop anything already running on 5432 (Postgres), 6379 (Redis), 9200 (Elasticsearch), 8080 (Spring), 8000 (FastAPI), 5173 (Frontend). Use lsof -i :PORT or sudo fuser -k PORT/tcp to free ports.
  • Stale volumes/config: Remove volumes when schema or config changes: docker compose down -v.
  • Env var drift: Missing .env keys cause startup errors (DB auth, JWT, API keys). Keep sample .env.example in sync.
  • Service startup race: Ensure DB/Redis are healthy before app starts (health checks + depends_on).
  • Memory limits: Elasticsearch can fail on low memory; adjust ES_JAVA_OPTS or allocate more resources.
  • File permissions: Volume-mounted logs or cache dirs may need proper ownership for non-root users.
  • Networking visibility: Containers resolve by service name (e.g., postgres); don’t use localhost from inside containers.

Lessons Learned

  • Image size optimization: Multi-stage builds; slim base images; --no-cache-dir; remove build tools from runtime.
  • Isolate configs per environment: Same variable names across .env, CI/CD, and prod reduce drift.
  • FastAPI vs Spring Boot: Python starts fast and is small; JVM needs warm-up but offers strong concurrency and tooling.
  • Debug container networking: Use container names, verify health checks, confirm exposed ports, and tail logs with docker compose logs -f.

Dockerizing a polyglot stack ensures reliable, reproducible environments end-to-end. With Compose locally and optimized images for production, we can iterate quickly and deploy confidently. Next up: deploying both services on Cloud Run with a CI/CD pipeline that builds, scans, and rolls out images automatically.

Tip: Keep sensitive values in .env locally and in a secret manager in production. Validate required vars early and fail fast when missing.

Top comments (0)