BoringServices
Deploy infrastructure services (Memcached, Redis, HAProxy, Nginx) to servers with Ruby and SSH. No Ansible, no Python, no Kubernetes.
Works standalone with any Ruby application or integrates seamlessly with Rails.
Features
- Memcached - In-memory caching with configurable parameters
- Redis - Key-value store and cache
- HAProxy - Load balancer with SSL/TLS support
- Nginx - Web server and reverse proxy
- Simple Configuration - YAML files with environment support
- Flexible Overrides - Custom config templates or parameter overrides
- Secret Management - Environment variables or command execution
- SSH Deployment - Direct deployment to Ubuntu servers
- Health Checks - Monitor service status across hosts
- VPN Support - Optional private_ip field for VPN/WireGuard deployments
- Rails Integration - Generators, rake tasks, and seamless integration
- Standalone Support - Works with any Ruby application
Quick Start
Standalone
gem install boring_services
boringservices setup
boringservices status
# Target a different config/environment
BORING_SERVICES_CONFIG=config/services.staging.yml \
BORING_SERVICES_ENV=staging boringservices setup
Rails Integration
# Gemfile
gem 'boring_services'
# Generate config
rails generate boring_services:install
# Deploy services
rails boring_services:setup
# Deploy using a different config/environment
BORING_SERVICES_CONFIG=config/services.staging.yml BORING_SERVICES_ENV=staging \
rails boring_services:setup
# Check health
rails boring_services:health
Configuration
Create config/services.yml:
production:
user: ubuntu
ssh_key: ~/.ssh/id_rsa
forward_agent: true
use_ssh_agent: false
ssh_auth_methods:
- publickey
services:
- name: memcached
enabled: true
hosts:
- host: 10.8.0.20
label: cache-a
- host: 10.8.0.21
label: cache-b
port: 11211
memory_mb: 256
- name: redis
enabled: true
host: 10.8.0.22
port: 6379
memory_mb: 1024
- name: haproxy
enabled: true
hosts:
- 10.8.0.30
port: 80
stats_port: 8404
backends:
- host: 10.8.0.10
port: 3000
- host: 10.8.0.11
port: 3000
secrets:
redis_password: $REDIS_PASSWORD
Use host: for a single target or hosts: for multiple VMs. When using hosts, each entry can be a simple hostname/IP or a hash with per-host overrides (e.g., label, port, or memory_mb).
Service Options
Memcached
Basic Configuration:
- name: memcached
enabled: true
hosts:
- 10.8.0.20
port: 11211 # Default: 11211
memory_mb: 256 # Memory allocation in MB
With Custom Parameters:
- name: memcached
enabled: true
host: 10.8.0.20
port: 11211
memory_mb: 512
custom_params:
listen_address: 0.0.0.0 # Default: 0.0.0.0
max_connections: 2048 # Default: 1024
max_item_size: 2m # Optional: max item size
verbosity: 2 # Optional: 0-3 for debug output
With Custom Config Template:
- name: memcached
enabled: true
host: 10.8.0.20
custom_config_template: config/memcached.custom.conf # Path to custom config file
Redis
- name: redis
enabled: true
hosts:
- 10.8.0.21
port: 6379 # Default: 6379
memory_mb: 512 # Memory allocation
Optional password protection via secrets:
secrets:
redis_password: $REDIS_PASSWORD
HAProxy
Basic HTTP Load Balancer:
- name: haproxy
enabled: true
host: 10.8.0.30
port: 80 # Frontend port
stats_port: 8404 # Stats dashboard port
backends:
- host: 10.8.0.10
port: 3000
- host: 10.8.0.11
port: 3000
HTTPS with SSL Termination:
- name: haproxy
enabled: true
host: 34.123.78.90
port: 80 # Redirects to HTTPS
https_port: 443 # SSL/TLS port
stats_port: 8404
ssl: true
ssl_cert: $(op read "op://MyVault/haproxy-ssl/certificate")
ssl_key: $(op read "op://MyVault/haproxy-ssl/private_key")
backends:
- host: 192.168.1.10
port: 3000
- host: 192.168.1.11
port: 3000
With Custom Parameters:
- name: haproxy
enabled: true
host: 10.8.0.30
port: 80
stats_port: 8404
custom_params:
timeout_connect: 10000 # Default: 5000ms
timeout_client: 60000 # Default: 50000ms
timeout_server: 60000 # Default: 50000ms
balance: leastconn # Default: roundrobin
health_check_path: /healthz # Default: /health
ssl_ciphers: CUSTOM_CIPHERS # Custom SSL cipher suite
ssl_options: no-sslv3 # Custom SSL options
backends:
- host: 10.8.0.10
port: 3000
With Custom Config Template:
- name: haproxy
enabled: true
host: 10.8.0.30
custom_config_template: config/haproxy.custom.cfg # Path to custom HAProxy config
# Note: When using custom template, most other options are ignored
SSL Certificate Options:
# Option 1: 1Password CLI
ssl_cert: $(op read "op://MyVault/haproxy-ssl/certificate")
ssl_key: $(op read "op://MyVault/haproxy-ssl/private_key")
# Option 2: Environment variables
ssl_cert: $HAPROXY_SSL_CERT
ssl_key: $HAPROXY_SSL_KEY
# Option 3: Rails credentials (Rails only)
ssl_cert: $(rails credentials:show | yq .haproxy.ssl.cert)
ssl_key: $(rails credentials:show | yq .haproxy.ssl.key)
# Option 4: Local files
ssl_cert: $(cat /path/to/cert.pem)
ssl_key: $(cat /path/to/key.pem)
HAProxy Features:
- ✅ Config Validation - Automatically validates HAProxy config before applying
- ✅ SSL/TLS Support - Automatic HTTPS setup with certificate management
- ✅ Self-Signed Fallback - Generates self-signed cert if none provided
- ✅ HTTP → HTTPS Redirect - Automatic redirect when SSL is enabled
- ✅ Health Checks - Configurable health check endpoint (default: GET /health)
- ✅ Stats Dashboard - Access at
http://your-host:8404/ - ✅ Custom Overrides - Override timeouts, balance algorithms, SSL ciphers
- ✅ Custom Templates - Bring your own HAProxy config file
Nginx
- name: nginx
enabled: true
hosts:
- 10.8.0.40
port: 80
ssl: true # Enable HTTPS on port 443
backends:
- host: 10.8.0.10
port: 3000
CLI Commands
Install Services
boringservices install
boringservices install redis
boringservices install -e staging
Check Status
boringservices status
boringservices status -e production
Restart Services
boringservices restart redis
boringservices restart haproxy -e staging
Uninstall Services
boringservices uninstall memcached
Rails Integration
Add to your Gemfile:
gem 'boring_services'
Generate configuration:
rails generate boring_services:install
# Creates config/services.yml
rails boring_services:setup
# Deploys all enabled services
Use services in your Rails app:
Rails.application.configure do
# Connect to Memcached
config.cache_store = :mem_cache_store, '10.8.0.20:11211', '10.8.0.21:11211'
# Connect to Redis for sessions
config.session_store :redis_store,
servers: ['redis://10.8.0.22:6379/0/session'],
expire_after: 90.minutes
end
Available Rake Tasks:
rails boring_services:setup # Deploy all services
rails boring_services:health # Check service health
rails boring_services:status # Show service status
rails boring_services:restart # Restart all services
Secrets Management
Use environment variables or command execution:
secrets:
# Option 1: Environment variable
redis_password: $REDIS_PASSWORD
# Option 2: 1Password CLI
redis_password: $(op read "op://MyVault/redis/password")
# Option 3: Rails credentials (Rails only)
redis_password: $(rails credentials:show | yq .redis.password)
# Option 4: File-based secrets
redis_password: $(cat .secrets/redis_password)
VPN/Private Network Support
BoringServices supports deploying services over VPN or private networks (e.g., WireGuard, Tailscale):
- Public IPs (
host) for SSH deployment access - Private IPs (
private_ip) for service communication - Some generated service configurations listen on all interfaces (
0.0.0.0) by default - Accessible via private network IPs
private_ipdocuments the address clients should use; it does not change the service bind address or configure a firewall
Before deploying, restrict each service port to the intended private CIDRs with the provider firewall and the host firewall, or set a supported service-specific listen address. A public SSH address does not make a service endpoint private.
Example with Private IPs:
services:
- name: redis
host: 18.234.67.89 # ← Public IP for SSH deployment
private_ip: 10.8.0.32 # ← Private VPN IP for connections
label: us-east-1
port: 6379
memory_mb: 1024
- name: memcached
host: 51.15.214.103 # ← Public IP for SSH
private_ip: 10.8.0.61 # ← Private VPN IP
label: cache-eu
port: 11211
memory_mb: 256
Your application connects via the private IP:
# Ruby/Rails app
Redis.new(url: 'redis://10.8.0.32:6379')
# Memcached
Dalli::Client.new('10.8.0.61:11211')
The private_ip field is optional and purely for documentation - use it to track which IPs your application should connect to.
Health Monitoring
boringservices status
Output:
memcached: healthy
✓ 10.8.0.20: running
✓ 10.8.0.21: running
redis: healthy
✓ 10.8.0.22: running
haproxy: healthy
✓ 10.8.0.30: running
Requirements
- Ruby 4.0+
- Ubuntu 20.04+ servers (Debian-based distributions)
- SSH key-based authentication
- Optional: VPN solution (WireGuard, Tailscale, etc.) for private networking
Installation
Standalone Ruby:
gem install boring_services
With Bundler:
# Gemfile
gem 'boring_services'
Then run:
bundle install
For Rails projects, the gem will automatically integrate with Rails and provide generators and rake tasks.
License
MIT