Skip to main content
rs-tunnel provides a Docker Compose configuration for running PostgreSQL and the API in containers.

Docker Compose Configuration

The repository includes a docker-compose.yml file with two services:
  1. postgres: PostgreSQL 16 database
  2. api: rs-tunnel API server
services:
  postgres:
    image: postgres:16-alpine
    restart: unless-stopped
    environment:
      POSTGRES_DB: ${POSTGRES_DB}
      POSTGRES_USER: ${POSTGRES_USER}
      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
    ports:
      - "23432:5432"
    volumes:
      - pgdata:/var/lib/postgresql/data

  api:
    build:
      context: .
      dockerfile: apps/api/Dockerfile
    depends_on:
      - postgres
    environment:
      DATABASE_URL: postgres://${POSTGRES_USER}:${POSTGRES_PASSWORD}@postgres:5432/${POSTGRES_DB}
      PORT: 8080
      API_BASE_URL: ${API_BASE_URL:-http://localhost:8080}
      JWT_SECRET: ${JWT_SECRET}
      REFRESH_TOKEN_SECRET: ${REFRESH_TOKEN_SECRET}
      SLACK_CLIENT_ID: ${SLACK_CLIENT_ID}
      SLACK_CLIENT_SECRET: ${SLACK_CLIENT_SECRET}
      SLACK_REDIRECT_URI: ${SLACK_REDIRECT_URI}
      ALLOWED_EMAIL_DOMAIN: ${ALLOWED_EMAIL_DOMAIN:[email protected]}
      ALLOWED_SLACK_TEAM_ID: ${ALLOWED_SLACK_TEAM_ID}
      CLOUDFLARE_ACCOUNT_ID: ${CLOUDFLARE_ACCOUNT_ID}
      CLOUDFLARE_ZONE_ID: ${CLOUDFLARE_ZONE_ID}
      CLOUDFLARE_API_TOKEN: ${CLOUDFLARE_API_TOKEN}
      CLOUDFLARE_BASE_DOMAIN: ${CLOUDFLARE_BASE_DOMAIN:-tunnel.example.com}
      JWT_ACCESS_TTL_MINUTES: 15
      REFRESH_TTL_DAYS: 30
      MAX_ACTIVE_TUNNELS: 5
      HEARTBEAT_INTERVAL_SEC: 20
      LEASE_TIMEOUT_SEC: 60
      REAPER_INTERVAL_SEC: 30
    ports:
      - ":8080"

volumes:
  pgdata:

Service Configuration

PostgreSQL Service

postgres
service
PostgreSQL 16 database using the official Alpine-based image.Key configuration:
  • Image: postgres:16-alpine (lightweight, production-ready)
  • Restart policy: unless-stopped (auto-restart on failure)
  • Port mapping: Host 23432 → Container 5432
  • Data persistence: pgdata named volume

Port Mapping

ports:
  - "23432:5432"
The database is exposed on host port 23432 (not the default 5432) to avoid conflicts with existing PostgreSQL installations.
Connection strings:
  • From host machine: postgres://postgres:postgres@localhost:23432/rs_tunnel
  • From API container: postgres://postgres:postgres@postgres:5432/rs_tunnel

Volume Mount

volumes:
  - pgdata:/var/lib/postgresql/data
Database data persists in the pgdata named volume. This ensures data survives container restarts.
To completely reset the database:
docker compose down -v  # Removes volumes
docker compose up -d postgres

Environment Variables

POSTGRES_DB
string
default:"rs_tunnel"
Database name to create on first startup.
POSTGRES_USER
string
default:"postgres"
PostgreSQL superuser name.
POSTGRES_PASSWORD
string
default:"postgres"
PostgreSQL superuser password.
Change this in production! Use strong passwords and environment-specific secrets.

API Service

api
service
rs-tunnel API server built from apps/api/Dockerfile.Key configuration:
  • Build context: Repository root (enables monorepo builds)
  • Depends on: postgres (waits for database to start)
  • Port: Exposes 8080 internally

Service Dependencies

depends_on:
  - postgres
Docker Compose ensures PostgreSQL starts before the API. However, this doesn’t guarantee the database is ready.
The API handles database connection retries automatically. If migrations fail due to database not being ready, wait a few seconds and retry.

Internal Networking

DATABASE_URL: postgres://${POSTGRES_USER}:${POSTGRES_PASSWORD}@postgres:5432/${POSTGRES_DB}
The API connects to PostgreSQL using the service name postgres as the hostname. Docker Compose creates an internal network where services resolve each other by name.
From outside Docker (like the CLI during development), use localhost:23432.

Running with Docker Compose

Development Setup

1

Create environment file

Create a .env file in the repository root:
cp .env.example .env
Edit .env with your configuration (see Environment Variables).
2

Start PostgreSQL only

For local development, run only the database:
docker compose up -d postgres
This allows you to run the API directly with pnpm for faster iteration.
3

Run migrations

pnpm --filter @ripeseed/api db:migrate
Use connection string: postgres://postgres:postgres@localhost:23432/rs_tunnel
4

Start API in development mode

pnpm --filter @ripeseed/api dev
API runs with hot-reload on port 8080.

Production Deployment

1

Set production environment variables

Create a production .env file:
.env.production
POSTGRES_DB=rs_tunnel
POSTGRES_USER=rs_tunnel_user
POSTGRES_PASSWORD=secure-random-password

API_BASE_URL=https://api.yourdomain.com
DATABASE_URL=postgres://rs_tunnel_user:secure-random-password@postgres:5432/rs_tunnel

# ... (all other required variables)
2

Build and start all services

docker compose --env-file .env.production up -d --build
  • --env-file: Use production environment
  • -d: Run in detached mode
  • --build: Rebuild API image
3

Check service status

docker compose ps
Both services should show “Up” status.
4

View logs

# All services
docker compose logs -f

# API only
docker compose logs -f api

# Postgres only
docker compose logs -f postgres

Health Checks

The Docker setup doesn’t include explicit health checks in docker-compose.yml, but you can verify service health:

PostgreSQL Health

docker compose exec postgres pg_isready -U postgres
Expected output:
/var/run/postgresql:5432 - accepting connections

API Health

Check if the API is responding:
curl http://localhost:8080/health
The API exposes a health endpoint (if implemented). Alternatively, check the logs for “Server listening on port 8080”.

Volume Management

Inspect Volume

docker volume inspect rs-tunnel_pgdata

Backup Database

# Dump database to file
docker compose exec postgres pg_dump -U postgres rs_tunnel > backup.sql

# Or use compressed format
docker compose exec postgres pg_dump -U postgres -Fc rs_tunnel > backup.dump

Restore Database

# From SQL file
docker compose exec -T postgres psql -U postgres rs_tunnel < backup.sql

# From compressed dump
docker compose exec -T postgres pg_restore -U postgres -d rs_tunnel < backup.dump

Common Docker Commands

docker compose up -d

Troubleshooting

Cause: Another service is using port 23432.Solution:
  1. Change the host port in docker-compose.yml:
    ports:
      - "25432:5432"  # Use different port
    
  2. Update DATABASE_URL to use new port
  3. Restart services:
    docker compose down
    docker compose up -d
    
Cause: Database not ready or connection string incorrect.Solution:
  1. Check PostgreSQL is running:
    docker compose ps postgres
    
  2. Verify DATABASE_URL uses service name postgres (not localhost):
    postgres://user:pass@postgres:5432/rs_tunnel
    
  3. Check PostgreSQL logs:
    docker compose logs postgres
    
Cause: Build order issue in monorepo.Solution: Ensure @ripeseed/shared is built before API:
pnpm --filter @ripeseed/shared build
docker compose build api
Cause: Volume was removed or not properly mounted.Solution:
  1. Don’t use docker compose down -v unless you want to delete data
  2. Verify volume exists:
    docker volume ls | grep pgdata
    
  3. Use regular stop/start:
    docker compose stop
    docker compose start
    

Production Considerations

The default docker-compose.yml is designed for local development. For production:
  1. Use managed PostgreSQL: AWS RDS, Google Cloud SQL, or Azure Database for PostgreSQL
  2. Change default credentials: Never use postgres:postgres in production
  3. Enable SSL: Add ?sslmode=require to DATABASE_URL
  4. Add health checks: Implement proper health check endpoints
  5. Use secrets management: HashiCorp Vault, AWS Secrets Manager, etc.
  6. Set resource limits: Add memory/CPU limits to service definitions
  7. Enable logging: Use logging drivers for centralized log collection
  8. Add monitoring: Prometheus, Grafana, or cloud-native monitoring

Example Production docker-compose.yml

docker-compose.production.yml
services:
  api:
    image: your-registry.com/rs-tunnel-api:latest
    restart: always
    environment:
      DATABASE_URL: ${DATABASE_URL}  # Managed DB connection string
      # ... other vars from secrets manager
    deploy:
      resources:
        limits:
          cpus: '2'
          memory: 2G
        reservations:
          cpus: '0.5'
          memory: 512M
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:8080/health"]
      interval: 30s
      timeout: 10s
      retries: 3
      start_period: 40s
    logging:
      driver: "json-file"
      options:
        max-size: "10m"
        max-file: "3"

Next Steps

Database Migration

Run Drizzle ORM migrations to initialize the schema

Environment Variables

Complete environment variable reference

Build docs developers (and LLMs) love