Skip to main content

Stack Patterns: Real-World Multi-Service Deployments with thresh

· 5 min read
thresh Team
thresh Development Team

thresh stacks turn multi-service applications into single-command deployments. But what does that look like for real applications? In this post, we cover battle-tested patterns for the most common architectures — from three-tier web apps to microservice meshes to data engineering pipelines.

Every example below is a complete, working stack definition you can save as a .json file and deploy with thresh stack up.

Pattern 1: Three-Tier Web Application

The classic: database, API, frontend. This is the most common pattern and the best place to start.

{
"name": "three-tier",
"services": [
{
"name": "postgres",
"image": "postgres:16",
"ports": ["5432:5432"],
"volumes": ["pgdata:/var/lib/postgresql/data"],
"env": {
"POSTGRES_USER": "app",
"POSTGRES_PASSWORD": "devpass",
"POSTGRES_DB": "appdb"
}
},
{
"name": "api",
"image": "node:20-alpine",
"ports": ["3000:3000"],
"depends_on": ["postgres"],
"env": {
"DATABASE_URL": "postgres://app:devpass@postgres:5432/appdb",
"NODE_ENV": "development"
}
},
{
"name": "web",
"image": "nginx:alpine",
"ports": ["8080:80"],
"depends_on": ["api"],
"traefik": true
}
]
}

Why it works:

  • depends_on ensures Postgres is ready before the API starts
  • Environment variables wire the services together using container hostnames
  • Traefik on the frontend handles reverse-proxy routing automatically
  • Named volume pgdata persists data across stack restarts

Deploy:

thresh stack up three-tier.json

Pattern 2: API + Cache + Worker

For applications that need background processing with a cache layer:

{
"name": "api-workers",
"services": [
{
"name": "redis",
"image": "redis:7-alpine",
"ports": ["6379:6379"]
},
{
"name": "postgres",
"image": "postgres:16",
"ports": ["5432:5432"],
"volumes": ["pgdata:/var/lib/postgresql/data"],
"env": { "POSTGRES_PASSWORD": "dev" }
},
{
"name": "api",
"image": "python:3.12-slim",
"ports": ["8000:8000"],
"depends_on": ["postgres", "redis"],
"env": {
"DATABASE_URL": "postgres://postgres:dev@postgres:5432/postgres",
"REDIS_URL": "redis://redis:6379/0",
"CELERY_BROKER": "redis://redis:6379/1"
}
},
{
"name": "worker",
"image": "python:3.12-slim",
"depends_on": ["redis", "postgres"],
"env": {
"DATABASE_URL": "postgres://postgres:dev@postgres:5432/postgres",
"CELERY_BROKER": "redis://redis:6379/1"
}
}
]
}

Key detail: The worker service has no ports — it doesn't accept inbound traffic. It only consumes from the Redis queue. Both api and worker depend on redis and postgres, so all infrastructure starts before either application service.


Pattern 3: Microservices with API Gateway

Multiple services behind a unified gateway:

{
"name": "microservices",
"services": [
{
"name": "rabbitmq",
"image": "rabbitmq:3-management",
"ports": ["5672:5672", "15672:15672"]
},
{
"name": "user-service",
"image": "myregistry/users:latest",
"ports": ["8081:8080"],
"depends_on": ["rabbitmq"],
"env": { "AMQP_URL": "amqp://guest:guest@rabbitmq:5672" }
},
{
"name": "order-service",
"image": "myregistry/orders:latest",
"ports": ["8082:8080"],
"depends_on": ["rabbitmq"],
"env": { "AMQP_URL": "amqp://guest:guest@rabbitmq:5672" }
},
{
"name": "notification-service",
"image": "myregistry/notifications:latest",
"depends_on": ["rabbitmq"],
"env": { "AMQP_URL": "amqp://guest:guest@rabbitmq:5672" }
},
{
"name": "gateway",
"image": "nginx:alpine",
"ports": ["80:80"],
"depends_on": ["user-service", "order-service"],
"traefik": true
}
]
}

Architecture:

  • RabbitMQ is the shared message bus
  • Each service registers independently
  • notification-service is a pure consumer (no ports)
  • The gateway routes external traffic to user-service and order-service

Updating a single service:

thresh stack update microservices --service order-service --image myregistry/orders:v2.0

Pattern 4: Data Engineering Pipeline

ETL pipeline with source database, processing, and analytics:

{
"name": "data-pipeline",
"services": [
{
"name": "source-db",
"image": "postgres:16",
"ports": ["5432:5432"],
"volumes": ["source-data:/var/lib/postgresql/data"],
"env": {
"POSTGRES_DB": "source",
"POSTGRES_PASSWORD": "dev"
}
},
{
"name": "redis",
"image": "redis:7-alpine",
"ports": ["6379:6379"]
},
{
"name": "etl-worker",
"image": "python:3.12-slim",
"depends_on": ["source-db", "redis"],
"env": {
"SOURCE_DB": "postgres://postgres:dev@source-db:5432/source",
"CACHE_URL": "redis://redis:6379/0"
}
},
{
"name": "analytics-db",
"image": "postgres:16",
"ports": ["5433:5432"],
"volumes": ["analytics-data:/var/lib/postgresql/data"],
"depends_on": ["etl-worker"],
"env": {
"POSTGRES_DB": "analytics",
"POSTGRES_PASSWORD": "dev"
}
},
{
"name": "dashboard",
"image": "metabase/metabase:latest",
"ports": ["3000:3000"],
"depends_on": ["analytics-db"],
"env": {
"MB_DB_TYPE": "postgres",
"MB_DB_HOST": "analytics-db",
"MB_DB_PORT": "5432",
"MB_DB_DBNAME": "analytics",
"MB_DB_PASS": "dev"
}
}
]
}

Flow: Source DB → ETL Worker → Analytics DB → Metabase Dashboard

Notice analytics-db uses host port 5433 to avoid conflicting with source-db on 5432.


Pattern 5: Development Tooling Stack

A stack of developer tools — useful for onboarding or standardizing team environments:

{
"name": "dev-tools",
"services": [
{
"name": "gitea",
"image": "gitea/gitea:latest",
"ports": ["3000:3000", "2222:22"],
"volumes": ["gitea-data:/data"]
},
{
"name": "registry",
"image": "registry:2",
"ports": ["5000:5000"],
"volumes": ["registry-data:/var/lib/registry"]
},
{
"name": "minio",
"image": "minio/minio:latest",
"ports": ["9000:9000", "9001:9001"],
"volumes": ["minio-data:/data"],
"env": {
"MINIO_ROOT_USER": "minioadmin",
"MINIO_ROOT_PASSWORD": "minioadmin"
}
}
]
}

No depends_on needed — these services are independent. They'll all start in parallel for fastest deployment.


Tips & Best Practices

1. Keep Stack Definitions in Your Repo

Store .json stack files alongside your application code. They become part of your versioned infrastructure:

my-app/
├── src/
├── stack.json
└── README.md

2. Use Rolling Updates for Zero-Downtime

When you build a new container image, update just that service:

thresh stack update my-app --service api --image myregistry/api:$(git rev-parse --short HEAD)

3. Separate Data Services from App Services

Data services (Postgres, Redis, RabbitMQ) rarely change images. Application services change frequently. Structure your dependencies so data services are at the bottom of the graph — they start first and get updated last.

4. Use Hub for Team Stacks

Deploy shared development stacks through the Hub so everyone on the team can see what's running:

thresh stack up team-env.json --hub https://hub.internal:7200

5. Name Your Volumes

Always use named volumes instead of anonymous ones. This makes it clear what data belongs to what service and lets you clean up selectively.


What's Next

These patterns cover most real-world use cases, but v2.0 will bring stack templates — parameterized definitions that let you define a pattern once and instantiate it with different configurations. Stay tuned.

Happy stacking! 📦