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 likePOSTGRES_PASSWORD
,JWT_SECRET
,GOOGLE_API_KEY
, etc., from a local.env
placed next todocker-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"]
- 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"]
- 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}"]
- 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: {}
-
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 (usedepends_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 }
-
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 }
- 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
anddocker compose logs -f backend backend-ai frontend
- App URLs: backend
http://localhost:8080
, AIhttp://localhost:8000
, frontendhttp://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"]
- 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"]
- 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). Uselsof -i :PORT
orsudo 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 uselocalhost
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)