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 legacy docker-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

  1. Compiles the Rust binary in release mode using a multi-stage Dockerfile
  2. Copies the binary into a minimal scratch-based image
  3. 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:

  1. Check Redis is running: docker compose ps
  2. Check the URL in .env matches the Compose port mapping
  3. 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:

  1. Make sure you are building in release mode (the default)
  2. Check that the final stage still uses the generated scratch-based runtime
  3. Strip the binary: add strip = true under [profile.release] in Cargo.toml
  4. Ensure debug symbols are not included: debug = false in release profile

Deployment advice

For first deployments:

  • keep Postgres and Redis as managed services or separate containers
  • use /health and /health/ready for liveness and readiness checks
  • keep JWT_SECRET out of source control
  • review the exported OpenAPI spec before exposing the app publicly
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

Back to top

Shaperail documentation lives in the same repository as the framework so every release has versioned instructions. See the latest release for the most recent version.

This site uses Just the Docs, a documentation theme for Jekyll.