Network Load Balancing
Convox v2 Racks can provision Network Load Balancers (NLBs) alongside the default Application Load Balancer. NLBs operate at Layer 4, allowing Services to accept traffic on arbitrary ports — raw TCP for protocols the ALB cannot handle (MQTT, Redis, raw TCP gRPC, game-server protocols) and TLS-terminating listeners when you need ACM-issued certificates on a non-HTTP port.
NLBs are opt-in at the Rack level and again per-Service. The existing ALB routing is unaffected.
Architecture
A Rack can host up to two shared NLBs:
- Public NLB — internet-facing, allocated one AWS Elastic IP per Availability Zone. Enabled via the NLB rack parameter.
- Internal NLB — VPC-internal (no EIPs), scheme
internal. Enabled via the NLBInternal rack parameter, which requires Internal=Yes.
Services opt in per-port via the nlb: field in convox.yml. Each declared port becomes a dedicated Listener and TargetGroup on the appropriate Rack NLB.
Within a single App's convox.yml, every NLB port must be unique across all Services regardless of scheme — the manifest validator rejects a second Service declaring the same port number, even if one is public and the other internal. Across Apps, the public NLB and internal NLB are separate AWS load balancers, so port 443 on App A's public listener and port 443 on App B's internal listener coexist without conflict. Two Apps claiming the same port on the same scheme is not caught at manifest time; the second deploy fails later at CloudFormation stack update (see "Two concurrent deploys" under Known Limitations).
Enabling the Rack NLBs
$ convox rack params set NLB=Yes
NLB provisioning typically takes 5-10 minutes; check status with 'convox rack'.
Before running this, verify your AWS account has Elastic IP quota headroom. A public NLB consumes 2 EIPs on a 2-AZ Rack or 3 EIPs on an HA 3-AZ Rack. If the Rack already has Private=Yes (which consumes 2-3 EIPs for NAT gateways), a Private=Yes HA 3-AZ Rack will need 6 EIPs total — above the AWS default of 5 per region.
$ aws service-quotas get-service-quota \
--service-code ec2 --quota-code L-0263D0A3
For internal NLB support:
$ convox rack params set Internal=Yes NLBInternal=Yes
After the CloudFormation update completes, convox rack will show the NLB DNS name(s) and allocated EIPs:
$ convox rack
Name production
Provider aws
Region us-east-1
Router router.0a1b2c3d4e5f.convox.cloud
NLB production-nlb-abc123.elb.us-east-1.amazonaws.com (52.1.2.3, 52.4.5.6, 52.7.8.9)
NLB Internal production-nlb-internal-xyz789.elb.us-east-1.amazonaws.com
Status running
Version 20260418101514
Clients connect directly to the NLB DNS hostname or to an EIP.
Service Configuration
Plain TCP
services:
mqtt-broker:
image: eclipse-mosquitto:2
nlb:
- port: 1883
protocol: tcp
scheme: public
TLS termination at the NLB
Attach an ACM or IAM server certificate to a listener with protocol: tls and a certificate: ARN. The NLB terminates TLS and forwards plaintext TCP to the container — backend Services do not need to hold the certificate material.
services:
api:
image: example/api
nlb:
- port: 443
protocol: tls
containerPort: 8080
scheme: public
certificate: arn:aws:acm:us-east-1:123456789012:certificate/abcd1234-5678-90ab-cdef-1234567890ab
TLS listeners use ELBSecurityPolicy-TLS13-1-2-2021-06 (TLS 1.2 and 1.3 with ECDHE ciphers). The target group protocol remains TCP — backends never see TLS traffic.
The certificate: field requires a full AWS ARN. convox certs lists both ACM-issued and IAM-imported certificates on the Rack, but displays them by Convox-synthesized short ID (ACM: acm-<hash>) or IAM server-certificate name — neither is a full ARN. Retrieve the ARN from AWS directly:
- ACM: copy from the AWS Console Certificate Manager page, or run
aws acm list-certificates --region <rack-region>. - IAM: construct as
arn:aws:iam::<account-id>:server-certificate/<name>using the nameconvox certsalready shows;aws sts get-caller-identityreturns the account ID.
Both ARN formats are accepted:
- ACM:
arn:aws:acm:<region>:<account>:certificate/<uuid> - IAM server-certificate:
arn:aws:iam::<account>:server-certificate/<name>
See services.nlb for the full field reference.
Mixing schemes and protocols
A Service can declare multiple nlb: entries combining public + internal schemes and tcp + tls protocols. A Service that needs both HTTP (via the ALB) and raw TCP (via the NLB) can use port: and nlb: together on the same containerPort.
services:
api:
image: example/api
port: 3000/http
nlb:
- port: 443
protocol: tls
containerPort: 3000
scheme: public
certificate: arn:aws:acm:us-east-1:123456789012:certificate/abcd1234-5678-90ab-cdef-1234567890ab
- port: 50051
protocol: tcp
containerPort: 50051
scheme: internal
Release-time validation
If a Service declares an nlb: port whose scheme does not match an enabled Rack NLB, the deploy is rejected at release promote with a clear error:
service api declares public nlb port 443 but rack does not have NLB enabled;
run 'convox rack params set NLB=Yes' first
TLS listeners are validated at release promote too — the referenced certificate ARN must exist in the Rack's region and account, and ACM certificates must be in ISSUED state. Typical failure messages:
certificate arn:aws:acm:us-east-1:123456789012:certificate/...: not found in
this region (is this cert in another region?)
certificate arn:aws:acm:us-east-1:123456789012:certificate/...: not usable
(status: PENDING_VALIDATION)
certificate arn:aws:acm:us-east-1:999999999999:certificate/...: access denied
(cross-account certificates are not supported)
certificate arn:aws:iam::123456789012:server-certificate/legacy: IAM server
certificate not found
These fail immediately on convox releases promote, not as an opaque CloudFormation error ten minutes later.
Viewing NLB ports on a Service
convox services adds an NLB PORTS column when any Service on the App declares nlb: ports. Format is PORT:CONTAINERPORT with /tls when the protocol is TLS and (internal) when the scheme is internal.
$ convox services -a broker
SERVICE DOMAIN PORTS NLB PORTS
api api.broker.0a1b2c.convox.cloud 443:3000 443:3000/tls 50051:50051(internal)
worker
Changing a listener's protocol
Switching an nlb: entry from protocol: tcp to protocol: tls (or vice versa) on an existing port modifies the listener in place via AWS ModifyListener. AWS documents this as a no-interruption update, though clients holding an open connection at the exact protocol-boundary moment may observe a brief disruption — switch during a low-traffic window.
Known Limitations
No real client IP
NLB target groups are configured with preserve_client_ip.enabled=false by design, so the per-Service security group rule (VPC CIDR ingress) covers both public and internal traffic uniformly. Application logs record the NLB's VPC-internal IP rather than the real client address. Compliance frameworks that require real client IPs (HIPAA §164.312(b), PCI-DSS 10.2.1) are not satisfied by this configuration — workloads with those requirements should terminate the Layer 4 listener in front of a proxy that injects the client IP, or wait for a future Convox option to toggle this attribute.
No operator-level CIDR allowlist
Ports declared with scheme: public are reachable from 0.0.0.0/0. There is no Rack-level allowlist in this release; restrict access at the application layer or with a custom security group on the target service.
Cross-zone load balancing is off
NLB inherits AWS's default — cross-zone load balancing is disabled. With per-AZ EIPs, clients hitting one EIP only reach targets in that AZ. Services with uneven target distribution across AZs may hotspot. This is not currently configurable through a Rack parameter.
50 listeners per NLB
The AWS default quota is 50 listeners per load balancer. Because Racks share one public NLB and one internal NLB across all Services, the combined total of scheme: public NLB ports (and similarly scheme: internal) across all Apps on the Rack cannot exceed 50 without a quota increase.
Disable procedure
To disable NLB on a Rack, first remove the nlb: block from every Service in every App and redeploy each. Only then will convox rack params set NLB=No succeed. The disable step releases the EIPs — if you re-enable later, the Rack will be assigned new EIPs and a new NLB DNS name. Re-validate any customer DNS pointing at the Rack after a disable/re-enable cycle.
Two concurrent deploys claiming the same port
Two Apps concurrently deploying with the same NLB listener port both pass release-promote validation (which only inspects the single App's manifest). One CloudFormation stack update succeeds; the other fails with an ELBv2 duplicate-listener error surfaced via stack events. Avoid concurrent deploys that claim the same NLB port across Apps, and confirm port allocation manually when multiple teams share a Rack.
Downgrade
Before downgrading a Rack to a version that predates NLB support, set both NLB=No and NLBInternal=No and wait for the CloudFormation update to complete. Otherwise the downgrade fails with Parameters: [NLB, NLBInternal] do not exist in the template.
NLB-only Services on EC2 launch type
A Service that declares only nlb: ports (no port: field) and runs on a default EC2-launch Rack (no Fargate, no Isolate) registers targets via the ECS service-linked role AWSServiceRoleForECS. AWS creates this role automatically on first ECS usage — if target registration fails on an NLB-only Service, confirm the role exists in the account. Fargate and Isolate Services use awsvpc mode and register targets by IP, so the role requirement does not apply.