Domains and HTTPS
Give a service a public URL, point your own domain at the server, and get HTTPS certificates.
Each web service and static site gets a URL when it deploys. To put your services on the internet, connect a domain that you own. Ferry then routes each name and gets a certificate for it.
ferry domains connect example.comThe browser asks DNS for the address of shop.example.com.
Each server runs one proxy. The proxy receives all the public traffic. It reads the hostname of each request and sends the request to an instance of the correct service.
The default URL
Each web service and static site answers at <name>.<domain> under each domain of the server. A server starts with one domain: its base domain. It comes from ferryd --base-domain, and the default is localhost. Browsers send each *.localhost name to 127.0.0.1. Thus on your own machine, you need no DNS setup.
The URL ends with the port of the proxy (--proxy-addr, default 8080): http://shop.localhost:8080. When HTTPS is on, a URL under a public domain has no port: https://shop.example.com.
| Service type | Public URL | Address for the other services |
|---|---|---|
| Web service | <name>.<domain>, and its custom domains | <name>:<port> |
| Static site | <name>.<domain>, and its custom domains | <name>:80 |
| Private service | None | <name>:<port> |
| Background worker, cron job | None | None. It can open connections. |
ferry open shop # print the URL of the service and try to open itAdd your own domain
You need control of the DNS of the domain, at your DNS provider. The internet must also reach the proxy on port 80. Start ferryd with --proxy-addr 0.0.0.0:80.
Ferry has two types of domain. You can use the two together.
| Domain of the server | Custom domain | |
|---|---|---|
| Covers | Each web service and static site | One service |
| Hostname | <service>.example.com, or <service>.<environment>.example.com outside production | www.example.com, or example.com itself |
| DNS | One wildcard DNS record | One record for each name |
| Command | ferry domains connect | ferry domains add |
On a public server, set the dashboard host
With the base domain localhost, the proxy also serves the dashboard and the API at the host ferry.localhost. On a server that the internet can reach, start ferryd with --dashboard-host none, or give it your own hostname with HTTPS.
Connect a domain to the server
Open Server → Domains. Click Connect a domain. Enter the domain, for example example.com. The window shows the DNS record to create.
Then create the DNS record. Ferry tests the domain each 15 seconds. When the names under the domain reach the server, the domain becomes active. Ferry then serves each web service and static site under it, with no deploy.
| Status | In the dashboard | Meaning |
|---|---|---|
pending | Waiting for DNS | The DNS does not point at the server. Ferry serves nothing under the domain. |
active | Active | Ferry serves each service under the domain. It tests the domain each 10 minutes. |
misconfigured | Misconfigured | The domain was active, then three checks in sequence failed. Ferry continues to serve it. |
The default domain is the domain that Ferry uses to show a URL: in the dashboard, in ferry open and in the variable FERRY_EXTERNAL_URL. If the default is a local name such as localhost, the first domain that becomes active becomes the default.
ferry domains # the domains, their status, the record to create
ferry domains default example.com # the domain service URLs are shown with
ferry domains disconnect example.com # services stop being served under itA service reads its new FERRY_EXTERNAL_URL at its next deploy or restart. A server can have 20 connected domains at most. You cannot disconnect the base domain. To change it, change --base-domain.
Add a custom domain to a service
A custom domain is one more hostname for one web service or static site. The proxy routes it immediately. No deploy is necessary.
ferry domains add shop shop.example.com
# Added shop.example.com. Point its DNS (A/AAAA or CNAME) at this server.
ferry domains shop # the custom domains of shop
ferry domains rm shop shop.example.com # remove oneferry create, ferry update and ferry up also accept --domain. You can repeat it.
A domain belongs to one service. Ferry refuses an IP address, the dashboard host, and a domain that another service has. It also refuses the default host of a service: <name>.<domain> under a domain of the server.
Create the DNS records
Add the records at your DNS provider. The value is the public IP address of the server. Ferry shows it when you connect a domain.
For a domain of the server:
| Type | Name | Value | Use |
|---|---|---|---|
A | * | The public IP address of the server | Necessary: each service |
A | @ | The same | Optional: only to serve the domain itself |
The name * is a wildcard: one record answers for each name under the domain. Thus a new service needs no new record.
For a custom domain:
| Domain | Record |
|---|---|
A subdomain, like shop.example.com | CNAME to a name that resolves to the server, or A/AAAA records |
The apex domain, like example.com | A (and AAAA) records with the IP addresses of the server |
A new record can take some minutes to show. Test the name, then test the domain:
dig +short shop.example.com # any name: shows the address when the record works
ferry domains verify example.com # a domain of the server: test it nowexample.com reaches this server: services are served at https://<service>.example.com
dns passed *.example.com resolves to 203.0.113.10, this server.
http passed This server answers on port 80 for names under example.com.Ferry makes a new name under the domain, for example ferry-check-<random>.example.com. The dns check passes when this name resolves to the server. The http check passes when a request for it on port 80 comes back to this server. Until the domain reaches the server, the command stops with the exit code 1. Thus a script can wait for the domain. In the dashboard, click Verify now.
The names of the records are relative to your DNS zone. Example: you connect apps.example.com, and the zone is example.com. Then name the records *.apps and apps. Ferry shows the two forms.
Ferry finds the IPv4 address of the server. Behind NAT, it asks api.ipify.org, ipv4.icanhazip.com or checkip.amazonaws.com. To give the address yourself, start ferryd with --public-ip 203.0.113.10. Ferry shows AAAA records only if you give an IPv6 address with --public-ip.
Get HTTPS certificates
Ferry gets a certificate for each hostname from Let’s Encrypt. Start ferryd with these options:
ferryd --base-domain apps.example.com \
--proxy-addr 0.0.0.0:80 --https-addr 0.0.0.0:443 \
--acme-email [email protected]The two options --https-addr and --acme-email are necessary. Without them, the domains work over HTTP only. Port 80 must stay open: Let's Encrypt connects to it for the HTTP-01 challenge.
Each host that the proxy routes gets its own certificate, a few seconds after the route starts. This includes the URL of each service under each domain of the server, and each custom domain. A local name and an IP address get no certificate. Ferry does not request wildcard certificates.
ferry certificatesHOST SERVICE CERTIFICATE
shop.example.com shop issued expires in 2moIn the dashboard, Server → Domains shows each hostname with the state of its certificate.
| State | Meaning |
|---|---|
pending, issuing | Ferry asks for the certificate. |
issued | Browsers get the certificate. Ferry renews it 30 days before it expires. |
failed | The last request failed. Ferry shows the cause and the time of the next attempt. |
local | A local name. Ferry serves it over HTTP. |
disabled | HTTPS is off on this server. |
Before the certificate is there, the proxy answers HTTPS with a self-signed certificate. After, each HTTP request gets a 308 redirect to HTTPS. Test the two:
curl -I https://shop.example.com
curl -I http://shop.example.com # 308 Permanent Redirect to https://shop.example.com/Private network and ports
Your app must listen on the address 0.0.0.0 and on the port in the variable PORT. Ferry publishes the port of each container on 127.0.0.1 of the server, and the proxy sends the requests there. An app that listens on 127.0.0.1 in its container gets no traffic. To choose the port, use ferry update shop --port 3000.
The containers of one environment share one Docker network: the private network. There, the name of a service or a datastore is its address. These requests do not go through the proxy.
curl http://api:3000/health # a private service named "api" listening on 3000
psql postgresql://…@app-db:5432/… # a datastore named "app-db"Do not write hosts and ports in your code. Use a reference: ${{service.api.hostport}} becomes api:3000 when the service starts. A private service has no public URL. ferry show api prints its internal address. Ferry does not isolate the services of one environment from each other on this network. A container of another environment cannot reach them by name. See Environments.
Each datastore also publishes its port on 127.0.0.1 of the server, never on a public interface. The external connection string of the datastore has this port. From your machine, open an SSH tunnel to it:
ssh -L 62975:127.0.0.1:62975 you@your-server # then use the external URL on your machineWebSockets and streamed answers go through the proxy. gRPC does not, because the proxy uses HTTP/1.1 with your app. Call a gRPC service on the private network.
Common problems
The verification fails
Run ferry domains to read the result of the last check.
| What Ferry found | Fix |
|---|---|
No DNS record answers for *.example.com | Create the record. Wait some minutes. |
| The name resolves to another address | Point the record to the address that Ferry shows. |
| The name resolves to many addresses | Remove the records that do not point to this server. |
| Another server answers | The record points to a different machine, or another program listens on port 80. |
| This server could not reach itself | A warning only. If visitors cannot reach the services, open port 80 in the firewall. |
No certificate
| What you see | Fix |
|---|---|
ferry certificates shows failed | Read the cause next to the state. The two frequent causes are the DNS and port 80. |
ferry info shows TLS: disabled | Start ferryd with --https-addr and --acme-email. |
| The rate limit of Let's Encrypt | Test with --acme-staging. Browsers do not trust these certificates. Remove the option after the test. |
The app makes http:// links | Read the scheme from the header X-Forwarded-Proto. |
After a failure, Ferry waits 5 minutes before the next attempt for that host. Then it waits 10 minutes, then 20, up to 6 hours. A restart of ferryd starts a new attempt immediately.
Errors from the proxy
When the proxy cannot send a request to your app, it answers with its own page and the header x-ferry-error.
| Status | x-ferry-error | Cause and fix |
|---|---|---|
404 | not_found | No service has this host. Run ferry domains shop to see the domains of the service. |
503 | no_upstreams | No instance runs: the service has no deploy, its last deploy failed, or a deploy is in progress. |
503 | suspended | You suspended the service. Resume it. |
502 | bad_gateway | The instance refused the connection. Make sure that the app listens on 0.0.0.0 and PORT. |