FerryDocs
Guides

Domains and HTTPS

Give a service a public URL, point your own domain at the server, and get HTTPS certificates.

Edit on GitHub

Each and 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 for it.

Terminal
ferry domains connect example.com
Private networkapi:3000app-db:5432BrowserDNSname → addressProxythe public sideshopweb serviceapiprivate serviceapp-dbdatastore

The browser asks DNS for the address of shop.example.com.

1 / 4

Each server runs one . The proxy receives all the public traffic. It reads the of each request and sends the request to an of the correct .

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 . 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 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 typeAddress 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 serviceNone<name>:<port>
Background worker, cron jobNoneNone. It can open connections.
Terminal
ferry open shop   # print the URL of the service and try to open it

Add your own domain

You need control of the DNS of the domain, at your . The internet must also reach the proxy on port 80. Start with --proxy-addr 0.0.0.0:80.

Ferry has two types of domain. You can use the two together.

CoversEach web service and static siteOne service
Hostname<service>.example.com, or <service>.<environment>.example.com outside productionwww.example.com, or example.com itself
DNSOne One record for each name
Commandferry domains connectferry 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.

StatusIn the dashboardMeaning
pendingWaiting for DNSThe DNS does not point at the server. Ferry serves nothing under the domain.
activeActiveFerry serves each service under the domain. It tests the domain each 10 minutes.
misconfiguredMisconfiguredThe domain was active, then three checks in sequence failed. Ferry continues to serve it.

The 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.

Terminal
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 it

A 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.

Terminal
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 one

ferry 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:

TypeNameValueUse
A*The public IP address of the serverNecessary: each service
A@The sameOptional: 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:

DomainRecord
A , like shop.example.comCNAME to a name that resolves to the server, or A/AAAA records
The , like example.comA (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:

Terminal
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 now
example.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 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 1. Thus a script can wait for the domain. In the dashboard, click Verify now.

Get HTTPS certificates

Ferry gets a certificate for each hostname from . Start ferryd with these options:

Terminal
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 .

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 and an IP address get no certificate. Ferry does not request wildcard certificates.

Terminal
ferry certificates
HOST               SERVICE   CERTIFICATE
shop.example.com   shop      issued        expires in 2mo

In the dashboard, Server → Domains shows each hostname with the state of its certificate.

StateMeaning
pending, issuingFerry asks for the certificate.
issuedBrowsers get the certificate. Ferry renews it 30 days before it expires.
failedThe last request failed. Ferry shows the cause and the time of the next attempt.
localA local name. Ferry serves it over HTTP.
disabledHTTPS is off on this server.

Before the certificate is there, the proxy answers HTTPS with a . After, each HTTP request gets a 308 to HTTPS. Test the two:

Terminal
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 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 share one Docker network: the . There, the name of a service or a is its address. These requests do not go through the proxy.

In a container
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 : ${{service.api.hostport}} becomes api:3000 when the service starts. A 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 to it:

Terminal
ssh -L 62975:127.0.0.1:62975 you@your-server   # then use the external URL on your machine

WebSockets 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 foundFix
No DNS record answers for *.example.comCreate the record. Wait some minutes.
The name resolves to another addressPoint the record to the address that Ferry shows.
The name resolves to many addressesRemove the records that do not point to this server.
Another server answersThe record points to a different machine, or another program listens on port 80.
This server could not reach itselfA warning only. If visitors cannot reach the services, open port 80 in the .

No certificate

What you seeFix
ferry certificates shows failedRead the cause next to the state. The two frequent causes are the DNS and port 80.
ferry info shows TLS: disabledStart ferryd with --https-addr and --acme-email.
The of Let's EncryptTest with --acme-staging. Browsers do not trust these certificates. Remove the option after the test.
The app makes http:// linksRead 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-errorCause and fix
404not_foundNo service has this host. Run ferry domains shop to see the domains of the service.
503no_upstreamsNo instance runs: the service has no deploy, its last deploy failed, or a deploy is in progress.
503suspendedYou suspended the service. Resume it.
502bad_gatewayThe instance refused the connection. Make sure that the app listens on 0.0.0.0 and PORT.

On this page