Skip to main content

Services

A Miren app can run multiple services—separate processes that work together as part of the same application. Each service can have its own command, image, environment variables, and scaling configuration.

What is a Service?

A service is a named process within your app. Common patterns include:

  • web: Your main HTTP server (receives external traffic)
  • worker: Background job processor
  • postgres: A database running alongside your app

Services share the same deployment lifecycle—when you deploy your app, all services are updated together. But each service scales independently and can run different code.

Defining Services

Services can be defined in two ways:

  1. Procfile — Simple format for defining commands per service
  2. .miren/app.toml — Full configuration with scaling, env vars, and images

Using a Procfile

If your app has a Procfile, Miren automatically infers services from it:

web: npm start
worker: npm run worker

Each line defines a service: the name before the colon, and the command after. This is compatible with Heroku's Procfile format.

For more control (scaling, environment variables, different images), use .miren/app.toml:

[services.web]
command = "npm start"

[services.worker]
command = "npm run worker"

[services.worker.concurrency]
mode = "fixed"
num_instances = 2

Same Image, Different Commands

The most common pattern is running multiple processes from the same codebase. Define a command for each service:

name = "myapp"

[services.web]
command = "npm start"

[services.worker]
command = "npm run worker"

Both services use your app's built image. The web service runs your HTTP server, while worker runs a background processor.

Example: Rails with Sidekiq

name = "railsapp"

[services.web]
command = "bundle exec puma -C config/puma.rb"

[services.worker]
command = "bundle exec sidekiq"

[services.worker.concurrency]
mode = "fixed"
num_instances = 2

Example: Python with Celery

name = "djangoapp"

[services.web]
command = "gunicorn myapp.wsgi:application --bind 0.0.0.0:8000"
port = 8000

[services.worker]
command = "celery -A myapp worker --loglevel=info"

[services.beat]
command = "celery -A myapp beat --loglevel=info"

Different Images

For services that need entirely different software—like a database—specify an image:

name = "myapp"

[services.web]
command = "npm start"

[services.postgres]
image = "postgres:16"

[services.postgres.concurrency]
mode = "fixed"
num_instances = 1

[[services.postgres.disks]]
name = "postgres-data"
mount_path = "/var/lib/postgresql/data"
size_gb = 20

When you specify an image, Miren pulls that container image instead of using your app's built image. This lets you run standard database images alongside your application code.

Example: Full Stack with PostgreSQL and Redis

name = "fullstack"

# Your application code
[services.web]
command = "node server.js"

[services.worker]
command = "node worker.js"

[services.worker.concurrency]
mode = "fixed"
num_instances = 2

# PostgreSQL database
[services.postgres]
image = "postgres:16"

[[services.postgres.env]]
key = "POSTGRES_PASSWORD"
value = "secret"

[services.postgres.concurrency]
mode = "fixed"
num_instances = 1

[[services.postgres.disks]]
name = "pg-data"
mount_path = "/var/lib/postgresql/data"
size_gb = 50

# Redis cache
[services.redis]
image = "redis:7-alpine"

[services.redis.concurrency]
mode = "fixed"
num_instances = 1

Service Configuration Reference

Each service can configure:

OptionDescriptionDefault
commandCommand to runImage's default entrypoint
imageContainer image to useApp's built image
portPort the web service listens on3000 (web only)
envService-specific environment variables(none)
concurrencyScaling configurationSee Scaling
disksPersistent disk attachments(none)

Environment Variables

Services inherit global environment variables from your app, and can add their own:

name = "myapp"

# Global env vars - available to all services
[[env]]
key = "LOG_LEVEL"
value = "info"

# Service-specific env vars
[services.web]
command = "npm start"

[[services.web.env]]
key = "NODE_ENV"
value = "production"

[services.worker]
command = "npm run worker"

[[services.worker.env]]
key = "WORKER_CONCURRENCY"
value = "5"

Service Communication

Services within the same app can communicate using internal DNS. Each service is discoverable at <service>.app.miren:

name = "myapp"

[[env]]
key = "DATABASE_URL"
value = "postgres://user:pass@postgres.app.miren:5432/mydb"

[[env]]
key = "REDIS_URL"
value = "redis://redis.app.miren:6379"

[services.web]
command = "npm start"

[services.postgres]
image = "postgres:16"

[[services.postgres.env]]
key = "POSTGRES_PASSWORD"
value = "pass"

[services.postgres.concurrency]
mode = "fixed"
num_instances = 1

[services.redis]
image = "redis:7-alpine"

[services.redis.concurrency]
mode = "fixed"
num_instances = 1

Connect to other services using their DNS name and standard port—postgres.app.miren:5432 for PostgreSQL, redis.app.miren:6379 for Redis. The container images listen on their standard ports by default; Miren doesn't manage these ports.

HTTP Routing

Only the web service receives external HTTP traffic. When you create a route to your app, requests go to the web service:

# Creates route to the web service
miren route add myapp.example.com --app myapp

Other services (workers, databases) are internal—they can't be reached from outside your app.

The web service defaults to port 3000. Override it if your app listens elsewhere:

[services.web]
command = "gunicorn app:app --bind 0.0.0.0:8000"
port = 8000

Service Scaling

Each service scales independently. By default:

  • web service: Autoscales based on traffic (scale-to-zero enabled)
  • All other services: Fixed at 1 instance

Configure scaling per-service:

[services.web.concurrency]
mode = "auto"
requests_per_instance = 20
scale_down_delay = "10m"

[services.worker.concurrency]
mode = "fixed"
num_instances = 3

For detailed scaling configuration, see Application Scaling.

Persistent Storage

Services can attach persistent disks. This is required for databases and other stateful workloads:

[services.postgres]
image = "postgres:16"

[services.postgres.concurrency]
mode = "fixed"
num_instances = 1

[[services.postgres.disks]]
name = "postgres-data"
mount_path = "/var/lib/postgresql/data"
size_gb = 20

Disks require fixed mode with exactly 1 instance because only one process can mount a disk at a time.

Sandbox Pools

When you deploy an app, Miren creates a sandbox pool for each service. The pool manages the desired number of instances (sandboxes) for that service.

The hierarchy is:

  • App → has one active deployment (version)
  • Sandbox Pool → one per service, manages instance count
  • Sandbox → individual running container

Inspecting What's Running

Use these commands to drill down from apps to running instances:

# List all apps and their current versions
miren app list
NAME          VERSION                              DEPLOYED  COMMIT
demo demo-vCVkjR6u7744AsMebwMjGU 1d ago 5f4dd55
conference conference-vCVkjJSe4fydvxEHfhsKfA 1d ago 5f4dd55
# List sandbox pools (one per service per version)
miren sandbox-pool list
ID                          VERSION                              SERVICE  DESIRED  CURRENT  READY
pool-CVkjTGJhRddyZDVq9CmnN demo-vCVkjR6u7744AsMebwMjGU web 1 1 1
pool-CVkjMv2R2VwcLdHJUoGKD conference-vCVkjJSe4fydvxEHfhsKfA web 3 3 3
pool-CVmuoeQCzjoNN9hGsu14c conference-vCVkjJSe4fydvxEHfhsKfA worker 2 2 2
# List individual sandboxes (instances)
miren sandbox list
ID                                SERVICE  POOL                        ADDRESS        STATUS
demo-web-CVok1wptmHEsJ6DmTRy7g web pool-CVkjTGJhRddyZDVq9CmnN 10.8.32.9/24 running
conference-web-CVnbNhSjUbGEAC5L web pool-CVkjMv2R2VwcLdHJUoGKD 10.8.32.12/24 running
conference-web-CVnbNhVDNcqapDcX web pool-CVkjMv2R2VwcLdHJUoGKD 10.8.32.19/24 running
# View logs for a specific sandbox
miren logs -s demo-web-CVok1wptmHEsJ6DmTRy7g

Complete Examples

Node.js API with Worker

name = "api"

[[env]]
key = "DATABASE_URL"
value = "postgres://user:pass@postgres.app.miren:5432/api"

[services.web]
command = "node dist/server.js"

[services.web.concurrency]
mode = "auto"
requests_per_instance = 50

[services.worker]
command = "node dist/worker.js"

[services.worker.concurrency]
mode = "fixed"
num_instances = 2

[services.postgres]
image = "postgres:16"

[[services.postgres.env]]
key = "POSTGRES_PASSWORD"
value = "pass"

[services.postgres.concurrency]
mode = "fixed"
num_instances = 1

[[services.postgres.disks]]
name = "pgdata"
mount_path = "/var/lib/postgresql/data"
size_gb = 10

Go Service with PostgreSQL

name = "goapp"

[[env]]
key = "DATABASE_URL"
value = "postgres://goapp:changeme@postgres.app.miren:5432/goapp"

[services.web]
command = "./server"

[services.web.concurrency]
mode = "auto"
requests_per_instance = 100
scale_down_delay = "5m"

[services.postgres]
image = "postgres:16-alpine"

[[services.postgres.env]]
key = "POSTGRES_USER"
value = "goapp"

[[services.postgres.env]]
key = "POSTGRES_PASSWORD"
value = "changeme"

[[services.postgres.env]]
key = "POSTGRES_DB"
value = "goapp"

[services.postgres.concurrency]
mode = "fixed"
num_instances = 1

[[services.postgres.disks]]
name = "pgdata"
mount_path = "/var/lib/postgresql/data"
size_gb = 10

Python App with Redis Queue

name = "taskqueue"

[[env]]
key = "REDIS_URL"
value = "redis://redis.app.miren:6379"

[services.web]
command = "gunicorn app:app --bind 0.0.0.0:8000"
port = 8000

[services.web.concurrency]
mode = "auto"
requests_per_instance = 20

[services.worker]
command = "rq worker --url redis://redis.app.miren:6379"

[services.worker.concurrency]
mode = "fixed"
num_instances = 3

[services.redis]
image = "redis:7-alpine"

[services.redis.concurrency]
mode = "fixed"
num_instances = 1

Next Steps