Skip to main content

Traffic Routing

Miren handles getting traffic to your app. For HTTP apps, this works automatically — deploy and add a route. For non-HTTP services like IRC servers, game servers, or custom TCP/UDP protocols, you configure ports explicitly.

Web Traffic (HTTP)

When you deploy an app, the web service automatically receives HTTP traffic. No port configuration is needed — Miren handles TLS, routing, and load balancing.

Adding a Route

Map a hostname to your app:

miren route add myapp.example.com --app myapp

Requests to that hostname are forwarded to your web service. TLS certificates are provisioned automatically (see TLS Certificates).

Wildcard Routes

Route all subdomains of a domain to a single app using a wildcard:

miren route set *.myapp.example.com --app myapp

A wildcard route *.myapp.example.com matches any subdomain like foo.myapp.example.com or bar.myapp.example.com. It does not match the bare domain myapp.example.com — add a separate route for that if needed.

If an exact route also exists for a specific subdomain, the exact route takes priority. For example, if you have both *.myapp.example.com and api.myapp.example.com, requests to api.myapp.example.com use the exact route while all other subdomains use the wildcard.

The wildcard * must be the first label — patterns like foo.*.example.com are not supported. The domain must have at least two labels after the wildcard (e.g., *.example.com is valid, *.com is not).

Choosing a Port

Miren sets the PORT environment variable to tell your app which port to listen on. Your app should bind to PORT:

[services.web]
command = "node server.js"
# App reads process.env.PORT and listens there

To choose a specific port, set it in .miren/app.toml:

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

Miren sets PORT=8000 and routes traffic there.

Non-HTTP Services (TCP/UDP)

To expose services that don't speak HTTP — an IRC server, a game server, a custom protocol — use the ports array in .miren/app.toml with a node_port to make the port reachable from outside the cluster.

[services.irc]
command = "./ircd"

[[services.irc.ports]]
port = 6667
name = "irc"
type = "tcp"
node_port = 6667

[[services.irc.ports]]
port = 6697
name = "irc-tls"
type = "tcp"
node_port = 6697

With this config:

  • The IRC server listens on ports 6667 and 6697 inside its container
  • Clients connect to those same ports on your Miren host
  • Miren forwards the traffic and load-balances across instances

Port Configuration

Each entry in ports accepts:

FieldRequiredDescriptionDefault
portYesPort your process listens on (1–65535)
nameYesA unique name for this port
typeNo"http" for web traffic, "tcp" for raw TCP, "udp" for UDP"http"
node_portNoPort to expose on the host machine (1–65535)(none)

Node Ports

The node_port is what makes a non-HTTP service reachable from outside. Without it, the port is only accessible to other services within the app.

Node port constraints:

  • Must be unique across all apps on the cluster — Miren checks for conflicts at deploy time
  • Cannot overlap with ports Miren uses (80, 443, 8443)

If your cloud provider uses security groups or network ACLs, you'll need to allow traffic on your node ports (see Firewall Configuration).

Multiple Ports per Service

A single service can expose a mix of HTTP and non-HTTP ports. This is useful when a service needs an HTTP endpoint for health checks or an API alongside a raw protocol port:

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

# HTTP endpoint for health checks and API
[[services.app.ports]]
port = 3000
name = "http"
type = "http"

# TCP port for the data protocol
[[services.app.ports]]
port = 7000
name = "data"
type = "tcp"
node_port = 7000

The HTTP port is routed through Miren's web traffic pipeline (routes, TLS, autoscaling). The TCP port is exposed directly via the node port. Both reach the same running process.

Same Port, Different Protocols

Some protocols need both TCP and UDP on the same port number. Declare them as separate entries:

[[services.dns.ports]]
port = 53
name = "dns-udp"
type = "udp"
node_port = 53

[[services.dns.ports]]
port = 53
name = "dns-tcp"
type = "tcp"
node_port = 5353

Port numbers can repeat as long as each port/type pair is unique within the service (since "tcp" and "udp" use different transport protocols).

The PORT Environment Variable

When using the ports array, Miren sets PORT to:

  1. The first port with type = "http", if one exists
  2. Otherwise, the first port in the array

PORT is managed by Miren and can't be overridden.

Service-to-Service Communication

Services within the same app can talk to each other using internal DNS. Each service is reachable at <service>.app.miren:

name = "myapp"

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

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

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

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

Internal traffic goes directly between containers — it doesn't pass through Miren's routing layer. Services connect on their standard ports (5432 for PostgreSQL, 6379 for Redis) without any Miren port configuration needed.

note

Internal DNS only works between services in the same app. Cross-app communication is not currently supported.

Simple Port Configuration

If your service only has one port, you can use the simpler single-field syntax instead of the ports array:

[services.web]
port = 8000
port_name = "http"
port_type = "http"
FieldDescriptionDefault
portPort your process listens onSet by PORT env var
port_nameName for the portService name
port_type"http" or "tcp""http"

You can't mix these fields with the ports array on the same service.

How It Works Under the Hood

info

You don't need to understand these details to use Miren. This section is for the curious.

HTTP traffic flows through Miren's HTTP ingress — a reverse proxy that listens on ports 80 and 443. When a request arrives, the ingress looks up the hostname to find the matching app, ensures a sandbox is running, and proxies the request to the sandbox's HTTP port.

Non-HTTP traffic is routed by the kernel using nftables NAT rules. When you deploy a service with non-HTTP ports, Miren allocates an internal IP for the service and programs firewall rules that forward matching traffic (by port and protocol) to the running sandboxes. As instances scale up and down, these rules are updated automatically to load-balance across available endpoints.

Examples

IRC Server

name = "irc"

[services.irc]
command = "./ircd"

[[services.irc.ports]]
port = 6667
name = "irc"
type = "tcp"
node_port = 6667

[[services.irc.ports]]
port = 6697
name = "irc-tls"
type = "tcp"
node_port = 6697

Game Server with HTTP Admin Panel

name = "gameserver"

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

[[services.game.ports]]
port = 3000
name = "admin"
type = "http"

[[services.game.ports]]
port = 27015
name = "game"
type = "udp"
node_port = 27015

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

TCP Echo Server

name = "tcp-echo"

[services.echo]
command = "./tcp-echo"

[[services.echo.ports]]
port = 3000
name = "health"
type = "http"

[[services.echo.ports]]
port = 7000
name = "echo"
type = "tcp"
node_port = 7000

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

Next Steps