Shaperail is Docker-first for local development. Generated apps should boot their local dependencies with Compose and should not require manual database setup for the first run.
Local development setup
Prerequisites
- Docker Engine 20+ or Docker Desktop
- Docker Compose v2 (
docker compose, not the legacydocker-compose)
Generated Compose file
shaperail init creates a docker-compose.yml that starts:
- PostgreSQL 16 on port 5432
- Redis 7 on port 6379
The generated .env matches the Compose file, so the default path is:
docker compose up -d
shaperail serve
Full local development Compose file
The generated docker-compose.yml looks like this:
version: "3.9"
services:
postgres:
image: postgres:16-alpine
restart: unless-stopped
environment:
POSTGRES_USER: shaperail
POSTGRES_PASSWORD: shaperail
POSTGRES_DB: my_app
ports:
- "5432:5432"
volumes:
- pg_data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U shaperail"]
interval: 5s
timeout: 3s
retries: 5
redis:
image: redis:7-alpine
restart: unless-stopped
ports:
- "6379:6379"
volumes:
- redis_data:/data
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 5s
timeout: 3s
retries: 5
volumes:
pg_data:
redis_data:
Matching .env file
DATABASE_URL=postgresql://shaperail:shaperail@localhost:5432/my_app
REDIS_URL=redis://localhost:6379
JWT_SECRET=dev-secret-change-in-production
# Optional process-level override if you do not want config port 3000
# SHAPERAIL_PORT=3001
Starting and stopping
# Start services in the background
docker compose up -d
# Check service health
docker compose ps
# View logs
docker compose logs -f postgres
docker compose logs -f redis
# Stop services (preserves data)
docker compose down
# Stop and remove all data
docker compose down -v
Standard local URLs
- App:
http://localhost:3000 - Docs:
http://localhost:3000/docs - OpenAPI:
http://localhost:3000/openapi.json - Health:
http://localhost:3000/health
Building release images with shaperail build --docker
Build a release image for a user app with:
shaperail build --docker
What the build does
- Compiles the Rust binary in release mode using a multi-stage Dockerfile
- Copies the binary into a minimal scratch-based image
- Tags the image as
<project-name>:latest
Generated Dockerfile
The framework generates a multi-stage Dockerfile:
# Generated by: shaperail build --docker
FROM rust:1.85-slim AS builder
RUN apt-get update && apt-get install -y musl-tools pkg-config ca-certificates \
&& rm -rf /var/lib/apt/lists/*
RUN rustup target add x86_64-unknown-linux-musl
WORKDIR /app
COPY . .
RUN cargo build --release --target x86_64-unknown-linux-musl
FROM scratch
COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ca-certificates.crt
COPY --from=builder /app/target/x86_64-unknown-linux-musl/release/my-app /my-app
USER 10001:10001
EXPOSE 3000
ENTRYPOINT ["/my-app"]
Build options
# Default build
shaperail build --docker
# Custom tag after the Dockerfile has been generated
docker build -t my-app:v1.2.0 .
# Extra Docker flags are passed to docker directly, not through shaperail
docker build --build-arg RUST_LOG=info -t my-app .
Image size target
The PRD target is a runtime image under 25 MB. The scratch-based final stage contains the compiled binary plus copied CA certificates for outbound TLS.
Running the release image locally
docker run --rm \
-e DATABASE_URL=postgresql://shaperail:shaperail@host.docker.internal:5432/my_app \
-e REDIS_URL=redis://host.docker.internal:6379 \
-e JWT_SECRET=dev-secret \
-p 3000:3000 \
my-app:latest
Note: use host.docker.internal to reach services running on the Docker host (the Compose services). On Linux, you may need --network host instead.
Multi-service Docker Compose
For workspace projects with multiple services, extend the Compose file:
version: "3.9"
services:
postgres:
image: postgres:16-alpine
environment:
POSTGRES_USER: shaperail
POSTGRES_PASSWORD: shaperail
POSTGRES_DB: my_workspace
ports:
- "5432:5432"
volumes:
- pg_data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U shaperail"]
interval: 5s
timeout: 3s
retries: 5
redis:
image: redis:7-alpine
ports:
- "6379:6379"
volumes:
- redis_data:/data
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 5s
timeout: 3s
retries: 5
api-gateway:
build:
context: ./services/api-gateway
depends_on:
postgres:
condition: service_healthy
redis:
condition: service_healthy
ports:
- "3000:3000"
environment:
DATABASE_URL: postgresql://shaperail:shaperail@postgres:5432/my_workspace
REDIS_URL: redis://redis:6379
JWT_SECRET: dev-secret
user-service:
build:
context: ./services/user-service
depends_on:
postgres:
condition: service_healthy
redis:
condition: service_healthy
ports:
- "3001:3000"
environment:
DATABASE_URL: postgresql://shaperail:shaperail@postgres:5432/my_workspace
REDIS_URL: redis://redis:6379
JWT_SECRET: dev-secret
order-service:
build:
context: ./services/order-service
depends_on:
postgres:
condition: service_healthy
redis:
condition: service_healthy
ports:
- "3002:3000"
environment:
DATABASE_URL: postgresql://shaperail:shaperail@postgres:5432/my_workspace
REDIS_URL: redis://redis:6379
JWT_SECRET: dev-secret
volumes:
pg_data:
redis_data:
Service networking
Inside Compose, services refer to each other by service name. The api-gateway service connects to Postgres at postgres:5432, not localhost:5432.
Running a workspace locally
# Start everything
docker compose up -d
# Or start specific services
docker compose up -d postgres redis api-gateway
# Or use shaperail workspace mode (requires services running outside Docker)
shaperail serve --workspace
Common Docker troubleshooting
Port conflicts
Symptom: Bind for 0.0.0.0:5432 failed: port is already allocated
Fix: Another process is using the port. Either stop it or change the host-side port in docker-compose.yml:
ports:
- "5434:5432" # Use 5434 on the host
Then update .env:
DATABASE_URL=postgresql://shaperail:shaperail@localhost:5434/my_app
Volume permissions
Symptom: Postgres fails to start with permission errors on the data directory.
Fix: Remove the volume and let Docker recreate it:
docker compose down -v
docker compose up -d
This deletes all data. If you need to preserve data, back it up with pg_dump first.
Container cannot reach host services
Symptom: The app container cannot connect to Postgres or Redis running on the host.
Fix: Use the special DNS name host.docker.internal:
DATABASE_URL=postgresql://shaperail:shaperail@host.docker.internal:5432/my_app
On Linux without Docker Desktop, add this to the service definition:
extra_hosts:
- "host.docker.internal:host-gateway"
Postgres not ready when app starts
Symptom: shaperail serve fails with “connection refused” on first boot.
Fix: The Compose file includes healthchecks. If you use depends_on, use the condition form:
depends_on:
postgres:
condition: service_healthy
If running outside Compose, wait for Postgres:
until pg_isready -h localhost -p 5432; do sleep 1; done
shaperail serve
Redis connection refused
Symptom: Error: Connection refused (os error 111) when the app tries to use cache or jobs.
Fix:
- Check Redis is running:
docker compose ps - Check the URL in
.envmatches the Compose port mapping - Test connectivity:
redis-cli -u redis://localhost:6379 ping
Build cache issues
Symptom: shaperail build --docker is slow or uses stale dependencies.
Fix: Bust the Docker build cache:
docker build --no-cache -t my-app .
Or manually:
docker build --no-cache -t my-app .
Large image size
Symptom: The built image is larger than the 25 MB target.
Fix:
- Make sure you are building in release mode (the default)
- Check that the final stage still uses the generated scratch-based runtime
- Strip the binary: add
strip = trueunder[profile.release]inCargo.toml - Ensure debug symbols are not included:
debug = falsein release profile
Deployment advice
For first deployments:
- keep Postgres and Redis as managed services or separate containers
- use
/healthand/health/readyfor liveness and readiness checks - keep
JWT_SECRETout of source control - review the exported OpenAPI spec before exposing the app publicly
Recommended production checklist
| Item | Why it matters |
|---|---|
JWT_SECRET comes from environment or secret manager | Prevents checked-in production credentials |
| Database and Redis URLs point at real services, not local containers | Separates production runtime from dev wiring |
| Health checks are wired into your platform | Makes rollout and restart behavior predictable |
| OpenAPI has been exported and reviewed | Confirms the public contract before launch |
strip = true in release profile | Keeps binary small |
| Resource limits set on containers | Prevents runaway memory or CPU usage |
| Postgres connection pool size matches worker count | Avoids pool exhaustion under load |
| TLS termination configured at load balancer | Encrypts traffic in transit |