Go Python PHP Drupal Docker Redis PostgreSQL

Structuring a Polyglot Project: Go, Python, and PHP in One Repository

Feb 23, 2026 4 min read

How I organized a multi-language codebase that's easy to develop, test, and deploy [6 min read]

TL;DR:

  • The Meeting Intelligence Platform uses Go (SSE service), Python (API, worker, Streamlit), and PHP (Drupal CMS) in a single repository.
  • Each language has its own directory with its own dependency management, Dockerfile, and tooling—no cross-language build complexity.
  • Docker Compose ties everything together for local development; each service is independently deployable in production.
  • The key insight: polyglot doesn't mean complicated. Keep services isolated, share only via APIs and message queues.

When I designed the Meeting Intelligence Platform, I chose different languages for different jobs:

  • Go for the SSE service — Low-memory, high-concurrency connection handling
  • Python for the API and worker — Rich AI/ML ecosystem, rapid development
  • PHP for Drupal — Client's existing CMS platform and team expertise

This isn't "polyglot for the sake of polyglot." Each language was chosen because it's the best tool for that specific job. The challenge is organizing the project so this multi-language setup doesn't become a maintenance nightmare.

For clients, this structure means we can safely introduce new services (like the Go SSE layer or a new AI worker) without destabilizing the existing Drupal site. Each piece is isolated, tested, and deployable on its own.

The Directory Structure

agile-creative-demos/
├── docker-compose.yml          # Orchestrates all services
├── Makefile                    # Common commands
├── .env.example                # Environment template
├── config/
│   └── keys/                   # RSA keys for service auth
├── meeting-intelligence/       # Python + Go services
│   ├── api/                    # FastAPI application
│   ├── worker/                 # Celery tasks
│   ├── shared/                 # Shared Python code
│   ├── streamlit/              # Standalone demo app
│   ├── sse/                    # Go SSE service (separate module)
│   │   ├── go.mod
│   │   ├── Dockerfile
│   │   └── *.go
│   ├── tests/
│   ├── pyproject.toml          # Python dependencies
│   └── Dockerfile              # Python services
├── drupal/                     # PHP CMS
│   ├── web/
│   │   └── modules/custom/     # Custom Drupal modules
│   ├── config/
│   ├── composer.json
│   └── Dockerfile
└── docs/
    ├── case-study/
    └── blog/

Key principles:

  1. Each language ecosystem is self-contained — Python has pyproject.toml, Go has go.mod, PHP has composer.json
  2. Shared code stays within language boundaries — Python services share via shared/, but nothing crosses language lines except HTTP/Redis
  3. Each deployable unit has its own Dockerfile — No multi-stage builds mixing languages

Why Not Separate Repositories?

I considered splitting into three repos (Python services, Go service, Drupal). Here's why I kept it together:

Pros of monorepo:

  • Single PR can update API schema and CMS integration together
  • Shared CI/CD configuration
  • Easier to keep documentation in sync
  • One docker-compose up starts everything

Cons of monorepo:

  • Larger clone size
  • CI runs for all languages even when only one changed
  • Teams working on different languages see each other's noise

For a solo developer or small team, the monorepo wins. The "single PR for cross-cutting changes" benefit alone is worth it. For larger teams with dedicated Go/Python/PHP specialists, separate repos might make sense.

The Go Service: Isolated Module

The SSE service is a Go module inside the Python project directory:

meeting-intelligence/
└── sse/
    ├── go.mod
    ├── go.sum
    ├── Dockerfile
    ├── main.go
    ├── server.go
    ├── hub.go
    ├── handlers.go
    ├── redis.go
    └── config.go

The go.mod makes this a standalone Go module:

module github.com/agilecreativeminds/sse-service

go 1.22

require github.com/redis/go-redis/v9 v9.7.0

It has its own Dockerfile with a multi-stage build:

# Build stage
FROM golang:1.22-alpine AS builder
WORKDIR /app
COPY go.mod ./
COPY *.go ./
RUN go mod tidy && go mod download
RUN CGO_ENABLED=0 GOOS=linux go build -o sse-service .

# Runtime stage
FROM alpine:3.19
WORKDIR /app
RUN apk --no-cache add ca-certificates
COPY --from=builder /app/sse-service .
EXPOSE 8080
CMD ["./sse-service"]

The Go service knows nothing about Python. It reads from Redis and serves HTTP. That's it.

The Python Services: Shared Codebase

Python handles multiple services from one codebase:

meeting-intelligence/
├── api/                    # FastAPI app
│   ├── main.py
│   ├── routers/
│   ├── models.py
│   └── ...
├── worker/                 # Celery worker
│   ├── celery_app.py
│   └── tasks.py
├── shared/                 # Shared code
│   ├── services/
│   └── data/
├── streamlit/              # Demo UI
│   └── app.py
├── pyproject.toml
└── Dockerfile

All Python services share the same pyproject.toml and Dockerfile. Docker Compose runs different commands:

# API service
api:
  build:
    context: .
    dockerfile: meeting-intelligence/Dockerfile
  command: uv run uvicorn api.main:app --host 0.0.0.0 --port 8000

# Celery worker
worker:
  build:
    context: .
    dockerfile: meeting-intelligence/Dockerfile
  command: uv run celery -A worker.celery_app worker --loglevel=info

# Streamlit app
streamlit:
  build:
    context: .
    dockerfile: meeting-intelligence/Dockerfile
  command: uv run streamlit run streamlit/app.py --server.port=8501

Same image, different entrypoints. This simplifies builds and ensures all Python services have identical dependencies.

The Drupal Service: Standard CMS Structure

Drupal follows its standard structure with custom modules:

drupal/
├── web/
│   ├── modules/
│   │   └── custom/
│   │       └── meeting_intelligence/    # API integration
│   │           ├── src/
│   │           │   ├── Controller/
│   │           │   └── Service/
│   │           ├── meeting_intelligence.module
│   │           └── meeting_intelligence.info.yml
│   └── themes/
├── config/
│   └── sync/                            # Exported config
├── composer.json
├── composer.lock
└── Dockerfile

The custom meeting_intelligence module contains all API integration code. Standard Drupal developers can work on theming and content without touching the integration layer.

Docker Compose: The Glue

Docker Compose orchestrates all services:

services:
  # Reverse proxy
  traefik:
    image: traefik:v2.11
    ports:
      - "8091:80"

  # Python API
  api:
    build:
      context: .
      dockerfile: meeting-intelligence/Dockerfile
    depends_on:
      - db
      - redis

  # Python Celery worker
  worker:
    build:
      context: .
      dockerfile: meeting-intelligence/Dockerfile
    depends_on:
      - db
      - redis

  # Go SSE service
  sse:
    build:
      context: ./meeting-intelligence/sse
      dockerfile: Dockerfile
    depends_on:
      - redis

  # PHP Drupal
  drupal:
    build:
      context: ./drupal
      dockerfile: Dockerfile
    depends_on:
      - drupal-db

  # Databases
  db:
    image: postgres:16-alpine
  
  drupal-db:
    image: mariadb:11.4

  # Shared infrastructure
  redis:
    image: redis:7-alpine

Each service has its own build context pointing to the right Dockerfile. Services communicate via:

  • HTTP — Drupal → API, browsers → all services
  • Redis — Worker → SSE (pub/sub for job updates)
  • PostgreSQL — API and Worker share the meetings database
  • MariaDB — Drupal's own database

No direct code sharing across languages. All integration happens through network protocols.

Development Workflow

Starting everything:

docker compose up -d

Working on Python code:

# API auto-reloads via uvicorn --reload
# Edit code, see changes immediately

Working on Go code:

# Rebuild just the SSE service
docker compose up -d --build sse

Working on Drupal:

# Changes to PHP files are immediate (volume mounted)
# For module changes, clear cache
docker compose exec drupal drush cr

Running Python tests:

docker compose exec api uv run pytest

Each language has its own tooling. No weird cross-language build scripts.

Production Deployment

In production, each service deploys independently:

  • Go SSE — Single binary, ~15MB container, scales horizontally
  • Python API/Worker — Separate deployments, shared image, different commands
  • Drupal — Standard PHP deployment, could be on separate infrastructure entirely

The monorepo is a development convenience. Production sees independent services that happen to share a git history.

CI/CD: In CI, each service can be built and tested independently (Go tests, Python tests, Drupal tests) before building production images. The monorepo ensures they share a single source of truth for configuration, while path-based triggers can skip unchanged services.

What I'd Do Differently

More shared configuration: Environment variables are duplicated across services. A shared config layer (Consul, AWS Parameter Store) would reduce duplication.

Unified logging format: Each language logs differently. A structured logging standard (JSON with consistent fields) would make aggregation easier.

Health check consistency: Go uses /health, Python uses /health, Drupal uses... nothing by default. Standardize this from day one.

Lessons Learned

1. Polyglot is fine if services are truly isolated

The Go service doesn't import Python. Python doesn't call PHP. They communicate through Redis and HTTP. This makes the polyglot aspect invisible day-to-day.

The mistake isn't using multiple languages; it's letting them share code or databases directly instead of talking over well-defined APIs and queues.

2. One Dockerfile per deployable unit

Don't try to build Go and Python in the same Dockerfile. Each language has its own build tooling and runtime. Keep them separate.

3. Docker Compose is the integration layer

Locally, docker-compose.yml is the single source of truth for how services connect. It documents the architecture better than any diagram.

4. Monorepo works for small teams

The ability to make cross-cutting changes in one PR outweighs the downsides. Split into multiple repos when team size demands it, not before.

Wrapping Up

A polyglot project doesn't have to be complicated. Keep each language ecosystem self-contained, communicate only through network protocols, and use Docker Compose to tie it all together. The result is a codebase where Go developers can work on Go, Python developers on Python, and PHP developers on PHP—without stepping on each other.

This structure works for any multi-language project: microservices, legacy integration, or just picking the best tool for each job.

You can see this architecture in action on the live demo—Go, Python, and PHP working together seamlessly.

Final post in the Meeting Intelligence Platform series.

What I designed and built for this project:

  • Project structure for Go + Python + Drupal in one repository
  • Docker and Docker Compose setup for local dev and production images
  • Service boundaries and communication via HTTP and Redis
  • Deployment strategy for independent service scaling

Building a multi-language system? I can help with:

  • Project structure for polyglot codebases
  • Docker and Docker Compose orchestration
  • CI/CD pipelines for multi-language builds
  • Service integration patterns

[Get in touch on Upwork] | [Get in Touch Directly →] to discuss your architecture.