Ingress and automatic TLS
How to expose applications on port 443 with certificates issued and renewed automatically, without operating an external router.
HeroCtl ships with an integrated router embedded in the control plane. You do not install, configure, or update it. When the cluster comes up, the router is already active on ports 80 and 443 of every server node.
That means exposing an application on the internet is a single line in the job spec.
The shortest path
job: minha-api
ingress:
host: api.exemplo.com
port: http
tls: true
With that the cluster does three things in sequence:
- Resolves the host
api.exemplo.comto any healthy allocation of the job. - Requests a valid certificate for that domain on the first request.
- Renews that certificate before expiry, with no intervention.
You only need to point the DNS for api.exemplo.com to the server node IPs. The rest runs by itself.
How the certificate is issued
HeroCtl uses Let's Encrypt as the default certificate authority. Behind that there are two supported validation mechanisms.
HTTP-01 (default)
Works for any public domain that points to the cluster. When an authority asks for proof of ownership, the integrated router responds at the /.well-known/acme-challenge/ path automatically. Nothing needs to be configured.
Warning: HTTP-01 requires port 80 to be externally accessible. If the firewall blocks 80, issuance fails.
DNS-01 (for wildcards)
If you need a wildcard certificate (*.exemplo.com), use DNS-01. Configure the DNS provider in the cluster spec:
acme:
challenge: dns-01
provider: cloudflare
credentials:
api_token: ${secret.cloudflare_token}
Natively supported providers: Cloudflare, Route53, DigitalOcean, Hostinger, Hetzner. Others go through an external plugin.
With DNS-01 active, just declare:
ingress:
host: "*.exemplo.com"
tls: true
HTTP → HTTPS redirect
When tls: true, all traffic arriving on port 80 is redirected with 301 to 443. You do not need to duplicate configuration.
To force HSTS:
ingress:
host: api.exemplo.com
tls: true
hsts:
enabled: true
max_age: 31536000
include_subdomains: true
preload: false
Do not enable preload without understanding the commitment. It is hard to reverse.
Multiple domains for the same app
Common pattern: www.exemplo.com and exemplo.com pointing to the same application, with one being canonical.
ingress:
host: exemplo.com
redirect_from:
- www.exemplo.com
tls: true
Each domain in redirect_from gets its own certificate and answers 301 to the canonical host.
Path-based routing
Same host, different applications on different paths:
# job: site-marketing
ingress:
host: exemplo.com
path: /
tls: true
# job: api-publica
ingress:
host: exemplo.com
path: /api
tls: true
The router resolves precedence by specificity. /api/users lands in api-publica. /sobre lands in site-marketing.
Custom headers
To add or remove headers in the response:
ingress:
host: api.exemplo.com
tls: true
headers:
add:
X-Frame-Options: DENY
X-Content-Type-Options: nosniff
Referrer-Policy: strict-origin
remove:
- Server
- X-Powered-By
Basic rate limiting
First-line defense against abuse:
ingress:
host: api.exemplo.com
tls: true
rate_limit:
requests_per_second: 100
burst: 200
by: ip
More elaborate limits (per token, per endpoint, with bypass for partners) belong in the application.
Troubleshooting
Certificate was not issued
Check three things, in order:
| Symptom | Likely cause | What to do |
|---|---|---|
dial tcp: timeout in the challenge log | Port 80 closed | Open 80/tcp inbound on server nodes |
unauthorized: Invalid response | DNS points elsewhere | dig +short api.exemplo.com should return a server node IP |
too many failed authorizations | You hit the Let's Encrypt rate limit | Wait 1h and review the config before trying again |
Inspect the issuance state:
heroctl ingress cert-status api.exemplo.com
Certificate expired without renewing
Renewal runs 30 days before expiry. If you are past that, errors have piled up:
heroctl ingress cert-renew api.exemplo.com --force
Also check whether redirect_from still points to the cluster. A domain that was moved to another provider without being removed from the spec breaks renewal silently.
Intermittent ACME challenge
Symptom: sometimes it issues, sometimes it fails. Usually it is DNS round-robin with one IP that does not respond on 80, or a Cloudflare in front in proxy mode. For HTTP-01, the domain needs to resolve directly to the cluster during the challenge. Use DNS-01 if you keep Cloudflare proxy on.