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:
| Field | Required | Description | Default |
|---|---|---|---|
port | Yes | Port your process listens on (1–65535) | — |
name | Yes | A unique name for this port | — |
type | No | "http" for web traffic, "tcp" for raw TCP, "udp" for UDP | "http" |
node_port | No | Port 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:
- The first port with
type = "http", if one exists - 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.
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"
| Field | Description | Default |
|---|---|---|
port | Port your process listens on | Set by PORT env var |
port_name | Name for the port | Service 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
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
- app.toml Reference — Complete field reference for port configuration
- Services — Defining services, commands, images, and scaling
- TLS Certificates — How HTTPS works for HTTP services
- Firewall Configuration — Host-level firewall rules and cloud provider setup
- Application Scaling — How services scale up and down