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)