Spry with Docker Compose: Multi‑Container Development Environment
Spry with Docker Compose: Multi‑Container Development Environment
Modern applications often depend on multiple services – databases, message queues, caches, and more. Managing these dependencies locally can be challenging. Docker Compose solves this by letting you define and run multi‑container Docker applications with a single command.
In this tutorial, we'll build a complete development environment for a Spry‑based microservice using Docker Compose. We'll containerize:
- Spry application – The Dart server with hot‑reload for development
- PostgreSQL – Primary database with persistent storage
- Redis – Caching and session storage
- RabbitMQ – Message queue for asynchronous processing
- Adminer – Database management UI (optional)
By the end, you'll have a reproducible, isolated environment that mirrors production and simplifies onboarding for new developers.
All code examples are taken from a working Spry project and can be run on your own machine.
Why Docker Compose for Spry Development?
- Consistency – Every developer gets the same service versions and configuration
- Isolation – No conflicts with system‑wide installations
- Reproducibility – Easy to share and version‑control the environment
- Production parity – Develop against the same stack you'll deploy
- Quick setup –
docker compose upbrings up the entire stack
Prerequisites
- Docker Desktop (or Docker Engine + Docker Compose plugin)
- Dart SDK (for local development, optional inside containers)
- Basic familiarity with Docker and Spry
Project Structure
spry‑compose‑example/
├── docker‑compose.yml # Multi‑container definition
├── Dockerfile.spry # Spry application image
├── lib/
│ └── main.dart # Spry application entry point
├── config/
│ └── database.env # Database connection settings
├── scripts/
│ └── wait‑for‑db.sh # Health‑check script
└── README.md
1. Docker Compose Configuration
Create docker‑compose.yml in your project root:
version: '3.8'
services:
# PostgreSQL database
postgres:
image: postgres:16-alpine
container_name: spry-postgres
environment:
POSTGRES_DB: sprydb
POSTGRES_USER: spryuser
POSTGRES_PASSWORD: sprypass
ports:
- "5432:5432"
volumes:
- postgres_data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U spryuser -d sprydb"]
interval: 10s
timeout: 5s
retries: 5
# Redis cache
redis:
image: redis:7-alpine
container_name: spry-redis
ports:
- "6379:6379"
volumes:
- redis_data:/data
command: redis-server --appendonly yes
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 10s
timeout: 5s
retries: 5
# RabbitMQ message broker
rabbitmq:
image: rabbitmq:3-management-alpine
container_name: spry-rabbitmq
environment:
RABBITMQ_DEFAULT_USER: spry
RABBITMQ_DEFAULT_PASS: sprypass
ports:
- "5672:5672" # AMQP
- "15672:15672" # Management UI
volumes:
- rabbitmq_data:/var/lib/rabbitmq
healthcheck:
test: ["CMD", "rabbitmqctl", "status"]
interval: 30s
timeout: 10s
retries: 5
# Spry application (development mode)
spry-app:
build:
context: .
dockerfile: Dockerfile.spry
target: development
container_name: spry-app
depends_on:
postgres:
condition: service_healthy
redis:
condition: service_healthy
rabbitmq:
condition: service_healthy
environment:
DATABASE_URL: postgres://spryuser:sprypass@postgres:5432/sprydb
REDIS_URL: redis://redis:6379
RABBITMQ_URL: amqp://spry:sprypass@rabbitmq:5672
NODE_ENV: development
ports:
- "8080:8080"
volumes:
- .:/app
- dart_cache:/root/.pub-cache
command: >
sh -c "
dart pub get &&
dart run --enable-asserts --enable-vm-service=8181 --disable-service-auth-codes lib/main.dart
"
# Adminer (database UI, optional)
adminer:
image: adminer
container_name: spry-adminer
depends_on:
- postgres
ports:
- "8081:8080"
environment:
ADMINER_DEFAULT_SERVER: postgres
volumes:
postgres_data:
redis_data:
rabbitmq_data:
dart_cache:
2. Spry Dockerfile
Create Dockerfile.spry with multi‑stage builds for development and production:
# ──────────────────────────────────────────────────────────────────────────────
# Development stage (hot‑reload, debugging tools)
# ──────────────────────────────────────────────────────────────────────────────
FROM dart:stable AS development
WORKDIR /app
# Install system dependencies (if needed)
RUN apt-get update && apt-get install -y \
postgresql-client \
curl \
&& rm -rf /var/lib/apt/lists/*
# Copy pubspec first for better layer caching
COPY pubspec.yaml pubspec.lock ./
RUN dart pub get
# Copy the rest of the application
COPY . .
# Expose VM service port for debugging
EXPOSE 8181
# Default command (overridden by docker‑compose in development)
CMD ["dart", "run", "--enable-vm-service=8181", "lib/main.dart"]
# ──────────────────────────────────────────────────────────────────────────────
# Production stage (minimal, optimized)
# ──────────────────────────────────────────────────────────────────────────────
FROM dart:stable-slim AS production
WORKDIR /app
# Install runtime dependencies only
RUN apt-get update && apt-get install -y \
ca-certificates \
tzdata \
&& rm -rf /var/lib/apt/lists/*
# Copy built application from builder (if using AOT compilation)
# For JIT, copy source and dependencies
COPY --from=development /app /app
RUN dart pub get --offline
# Run as non‑root user
RUN groupadd -r spry && useradd -r -g spry spry
USER spry
EXPOSE 8080
CMD ["dart", "run", "lib/main.dart"]
3. Spry Application with Multi‑Service Integration
Update your Spry application to connect to the containerized services:
// lib/main.dart
import 'dart:io';
import 'package:spry/spry.dart';
import 'package:spry_postgres/spry_postgres.dart';
import 'package:spry_redis/spry_redis.dart';
import 'package:spry_amqp/spry_amqp.dart';
Future<void> main() async {
final app = Application();
// Load environment variables
final databaseUrl = Platform.environment['DATABASE_URL'] ??
'postgres://spryuser:sprypass@localhost:5432/sprydb';
final redisUrl = Platform.environment['REDIS_URL'] ??
'redis://localhost:6379';
final rabbitmqUrl = Platform.environment['RABBITMQ_URL'] ??
'amqp://spry:sprypass@localhost:5672';
// Connect to PostgreSQL
final postgres = await Postgres.connect(databaseUrl);
app.use(postgres.middleware());
// Connect to Redis
final redis = await Redis.connect(redisUrl);
app.use(redis.middleware());
// Connect to RabbitMQ
final amqp = await Amqp.connect(rabbitmqUrl);
app.use(amqp.middleware());
// Health check endpoint
app.get('/health', (request) async {
final checks = <String, String>{};
try {
await postgres.query('SELECT 1');
checks['postgres'] = 'healthy';
} catch (e) {
checks['postgres'] = 'unhealthy: ${e.toString()}';
}
try {
await redis.ping();
checks['redis'] = 'healthy';
} catch (e) {
checks['redis'] = 'unhealthy: ${e.toString()}';
}
try {
await amqp.channel();
checks['rabbitmq'] = 'healthy';
} catch (e) {
checks['rabbitmq'] = 'unhealthy: ${e.toString()}';
}
return Response.json(checks);
});
// Your application routes
app.get('/', (request) async {
return Response.text('Spry with Docker Compose 🐳');
});
// Start server
final server = await app.listen(port: 8080);
print('Server running on http://${server.address.host}:${server.port}');
}
4. Development Workflow
Starting the Environment
# Clone the example repository
git clone https://github.com/yourusername/spry-compose-example.git
cd spry-compose-example
# Start all services
docker compose up -d
# View logs
docker compose logs -f spry-app
# Check service status
docker compose ps
Hot‑Reload Development
Because we mount the local directory as a volume in the spry‑app container, changes to your Dart code trigger automatic reloads (when using dart run). The development Dockerfile includes the VM service for debugging.
Accessing Services
- Spry application: http://localhost:8080
- PostgreSQL:
localhost:5432(username:spryuser, password:sprypass, database:sprydb) - Redis:
localhost:6379 - RabbitMQ management: http://localhost:15672 (username:
spry, password:sprypass) - Adminer: http://localhost:8081 (connect to
postgresserver)
Running Commands Inside Containers
# Run Dart tests
docker compose exec spry-app dart test
# Open a shell in the app container
docker compose exec spry-app sh
# Check database health
docker compose exec postgres pg_isready -U spryuser -d sprydb
5. Production Considerations
While this setup is ideal for development, production deployments require additional considerations:
Separate Compose File for Production
Create docker‑compose.prod.yml with:
- Removed volume mounts (code baked into image)
- Resource limits (CPU, memory)
- Production‑ready logging (JSON format, centralized)
- Security hardening (non‑root users, read‑only filesystems)
- Health checks with restart policies
Environment‑Specific Configuration
Use Docker Compose profiles or separate files:
# Development
docker compose -f docker-compose.yml -f docker-compose.dev.yml up
# Production
docker compose -f docker-compose.yml -f docker-compose.prod.yml up
CI/CD Integration
Example GitHub Actions workflow:
name: Build and Deploy
on: [push]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Build Docker images
run: |
docker compose -f docker-compose.yml build
- name: Run tests
run: |
docker compose -f docker-compose.yml run --rm spry-app dart test
- name: Push to registry
run: |
docker tag spry-app:latest yourregistry/spry-app:${{ github.sha }}
docker push yourregistry/spry-app:${{ github.sha }}
6. Troubleshooting Common Issues
Container Startup Order
Use depends_on with condition: service_healthy and implement wait scripts for services that don't have built‑in health checks.
Permission Issues with Volumes
Ensure your local user ID matches the container user, or adjust permissions in the Dockerfile.
Dart Pub Cache Performance
The dart_cache volume preserves the pub cache across container rebuilds, significantly speeding up dart pub get.
Network Connectivity Between Containers
Use Docker Compose service names (e.g., postgres, redis) as hostnames within the Docker network. Ports exposed only to other containers don't need to be published to the host.
Conclusion
Docker Compose provides an excellent way to manage multi‑service development environments for Spry applications. With a single docker compose up command, you get a complete, isolated stack that mirrors production.
This setup improves developer experience, reduces environment‑specific bugs, and makes onboarding new team members faster. As your Spry application grows, you can easily add more services (Elasticsearch, Kafka, etc.) to the same Compose file.
Next Steps:
- Extend the example – Add more services (Elasticsearch, MinIO, etc.)
- Implement CI/CD – Automate testing and deployment
- Explore production orchestration – Move to Kubernetes when ready
- Monitor and optimize – Use Docker stats and logging to track performance
All code from this tutorial is available in the spry‑compose‑example GitHub repository.
Happy containerizing! 🐳