| 1 | --- |
| 2 | title: IPs & certificates |
| 3 | description: Public and private Fly Services, App addresses, and certificates for your own hostname. |
| 4 | --- |
| 5 | |
| 6 | A [Service](/fly/compute/services) owns its App and manages that |
| 7 | App's addresses. It is public by default and reachable only inside |
| 8 | your organization with `public: false`. Fly's proxy load-balances |
| 9 | `{app}.fly.dev` across Machines that publish a proxy service. |
| 10 | |
| 11 | IPs and certificates you declare yourself attach to a |
| 12 | [`Fly.App`](/fly/compute/apps): use them for Machines, for Services |
| 13 | grouped in one App, and for your own domain. |
| 14 | |
| 15 | ## Public Services |
| 16 | |
| 17 | A Service with `port` listens inside the Machine. Alchemy publishes |
| 18 | HTTP 80 and HTTPS 443 on the Fly proxy in front of it, and allocates |
| 19 | a shared IPv4 and an IPv6 on the Service's App. Both are free. |
| 20 | |
| 21 | ```typescript |
| 22 | export default class Api extends Fly.Service<Api>()( |
| 23 | "Api", |
| 24 | { main: import.meta.url, region: "iad", port: 3000 }, |
| 25 | Effect.gen(function* () { |
| 26 | return { |
| 27 | fetch: Effect.succeed(HttpServerResponse.text("hello")), |
| 28 | }; |
| 29 | }), |
| 30 | ) {} |
| 31 | ``` |
| 32 | |
| 33 | Yield the Service in the Stack. `api.url` is |
| 34 | `https://{appName}.fly.dev`. |
| 35 | |
| 36 | ```typescript |
| 37 | export default Alchemy.Stack( |
| 38 | "MyApp", |
| 39 | { providers: Fly.providers(), state: Alchemy.localState() }, |
| 40 | Effect.gen(function* () { |
| 41 | const api = yield* Api; |
| 42 | return { url: api.url }; |
| 43 | }), |
| 44 | ); |
| 45 | ``` |
| 46 | |
| 47 | Fly's proxy terminates TLS on 443. The Service still listens on |
| 48 | `port` inside the Machine. |
| 49 | |
| 50 | ## Private Services with Flycast |
| 51 | |
| 52 | `public: false` keeps a Service off the internet. Its App gets only a |
| 53 | free Flycast address, which Fly's proxy serves inside your |
| 54 | organization's private network. |
| 55 | |
| 56 | ```diff lang="typescript" |
| 57 | export default class Api extends Fly.Service<Api>()( |
| 58 | "Api", |
| 59 | - { main: import.meta.url, region: "iad", port: 3000 }, |
| 60 | + { main: import.meta.url, region: "iad", port: 3000, public: false }, |
| 61 | ``` |
| 62 | |
| 63 | Fly issues no TLS certificate for `.flycast`, so a private Service |
| 64 | publishes plain HTTP on port 80. `url` is `undefined` and |
| 65 | `privateUrl` is `http://{appName}.flycast`. |
| 66 | |
| 67 | Other Services call it by binding it with `Fly.bindService`, which |
| 68 | returns a typed client for its methods — see |
| 69 | [Call one Service from another](/fly/compute/services#call-one-service-from-another). |
| 70 | Bound calls go to `privateUrl` through Fly's proxy, so service checks, |
| 71 | `autostart`, and the traffic switch of a |
| 72 | [blue/green deployment](/fly/compute/deployments) apply. Only Services |
| 73 | that bind the target receive its caller token, and the target checks |
| 74 | Fly's `Fly-Src` signature, so a method answers only bound callers on |
| 75 | Fly's private network. The traffic is plain HTTP inside Fly's |
| 76 | WireGuard-encrypted network. |
| 77 | |
| 78 | Turning `public` on or off updates the Service in place. Its App and |
| 79 | hostname stay the same. |
| 80 | |
| 81 | ## Restrict private Services to the stack |
| 82 | |
| 83 | Fly's default private network spans the organization, so every App in |
| 84 | the org can call a private Service. Set `network: yield* Fly.stackNetwork` |
| 85 | on each Service to put them on a network unique to the stack and |
| 86 | stage. Services on it bind each other; Apps on any other network, |
| 87 | including the default one, get `bad address` for the `.flycast` and |
| 88 | `.internal` names, and binding across networks fails the caller's |
| 89 | deploy with `Fly.ServiceUnreachable`. A public Service on the network |
| 90 | still serves `url`, so it becomes the stack's single entry point. |
| 91 | [Connect Services](/fly/compute/connecting-services) walks through it. |
| 92 | |
| 93 | ## Addresses for a shared App |
| 94 | |
| 95 | A [Machine](/fly/compute/machines), or a Service that joins a |
| 96 | `Fly.App` with [`app`](/fly/compute/services#group-services-in-one-app), |
| 97 | uses the addresses on that App. Alchemy manages none for it. |
| 98 | `{app}.fly.dev` does not answer over IPv4 until the App has an |
| 99 | [`IpAssignment`](/providers/fly/ipassignment). Allocate a shared |
| 100 | Anycast IPv4: |
| 101 | |
| 102 | ```typescript |
| 103 | export const Site = Fly.App("Site"); |
| 104 | |
| 105 | export const PublicIp = Fly.IpAssignment("Shared", { |
| 106 | app: Site, |
| 107 | type: "shared_v4", |
| 108 | }); |
| 109 | ``` |
| 110 | |
| 111 | Yield it next to the resources in the App. |
| 112 | |
| 113 | ```diff lang="typescript" |
| 114 | Effect.gen(function* () { |
| 115 | const api = yield* Api; |
| 116 | + const ip = yield* PublicIp; |
| 117 | return { url: api.url, ip: ip.ip }; |
| 118 | }), |
| 119 | ``` |
| 120 | |
| 121 | `v6` is free dedicated IPv6. `v4` is billed dedicated IPv4 and may |
| 122 | 400 if the org has no quota. Prefer `shared_v4` or `v6` in tests. |
| 123 | |
| 124 | `private_v6` is a free Flycast address that is not reachable from the |
| 125 | internet. Allocate only `private_v6` on an App to keep everything in |
| 126 | it private: |
| 127 | |
| 128 | ```typescript |
| 129 | export const Backend = Fly.App("Backend"); |
| 130 | |
| 131 | export const BackendIp = Fly.IpAssignment("Flycast", { |
| 132 | app: Backend, |
| 133 | type: "private_v6", |
| 134 | }); |
| 135 | ``` |
| 136 | |
| 137 | Resources in the App still publish a port for the proxy to forward |
| 138 | to. Publish plain HTTP on port 80 without `forceHttps`: |
| 139 | |
| 140 | ```typescript |
| 141 | export default class Worker extends Fly.Service<Worker>()( |
| 142 | "Worker", |
| 143 | { |
| 144 | app: Backend, |
| 145 | main: import.meta.url, |
| 146 | port: 3000, |
| 147 | services: [{ |
| 148 | protocol: "tcp", |
| 149 | internalPort: 3000, |
| 150 | ports: [{ port: 80, handlers: ["http"] }], |
| 151 | }], |
| 152 | }, |
| 153 | /* ... */ |
| 154 | ) {} |
| 155 | ``` |
| 156 | |
| 157 | Other Apps on the App's network reach it at `http://{appName}.flycast`. |
| 158 | A Service in the App is also [bindable](/fly/compute/services#bind-a-service-in-a-shared-app): |
| 159 | its deploy adds the Flycast address itself. |
| 160 | |
| 161 | `network` places the address on a named private network instead of the |
| 162 | organization default. The network must already exist: an App created with |
| 163 | the same `network` creates it, and an unknown name fails with |
| 164 | `NetworkNotFound`. Changing `network` replaces the assignment. |
| 165 | |
| 166 | ## Use your own hostname |
| 167 | |
| 168 | A Certificate covers a hostname on a `Fly.App`. It cannot target a |
| 169 | Service's own App yet, so run the Service in that App with |
| 170 | [`app: Site`](/fly/compute/services#group-services-in-one-app) and |
| 171 | allocate its addresses there. Default `kind` is `"acme"` (Let's |
| 172 | Encrypt). |
| 173 | |
| 174 | ```typescript |
| 175 | export const V6 = Fly.IpAssignment("V6", { |
| 176 | app: Site, |
| 177 | type: "v6", |
| 178 | }); |
| 179 | |
| 180 | export const Www = Fly.Certificate("Www", { |
| 181 | app: Site, |
| 182 | hostname: "www.example.com", |
| 183 | kind: "acme", |
| 184 | }); |
| 185 | ``` |
| 186 | |
| 187 | Yield `Www` in the Stack. Point DNS at the App. An A record for |
| 188 | `www` to `PublicIp.ip`. An AAAA record to `V6.ip`. Plus whatever |
| 189 | `Www.dnsRequirements` lists for the ACME challenge. |
| 190 | |
| 191 | Fly's proxy terminates TLS on 443 once the certificate is |
| 192 | `configured`. |
| 193 | |
| 194 | `"custom"` uploads a PEM (`fullchain` + `privateKey`). Hostname is |
| 195 | the identity. Changing `app`, `hostname`, or `kind` replaces. |
| 196 | |
| 197 | ## Issue a certificate independently |
| 198 | |
| 199 | Use `alchemy/ACME` when you want to choose the certificate authority or |
| 200 | issue a wildcard with DNS-01. The account and certificate are independent |
| 201 | of Fly; this example uploads the result to a Fly App. |
| 202 | |
| 203 | ```typescript |
| 204 | import * as ACME from "alchemy/ACME"; |
| 205 | import * as Cloudflare from "alchemy/Cloudflare"; |
| 206 | import * as Fly from "alchemy/Fly"; |
| 207 | import * as Layer from "effect/Layer"; |
| 208 | |
| 209 | const providers = Layer.mergeAll( |
| 210 | ACME.providers(), |
| 211 | Cloudflare.providers(), |
| 212 | Fly.providers(), |
| 213 | ); |
| 214 | ``` |
| 215 | |
| 216 | Use `providers` in the Stack configuration. Inside the Stack's Effect, |
| 217 | resolve your existing Cloudflare `Zone` and Fly `Site`, then issue and upload: |
| 218 | |
| 219 | ```typescript |
| 220 | const zone = yield* Zone; |
| 221 | const site = yield* Site; |
| 222 | const account = yield* ACME.Account("LetsEncrypt", { |
| 223 | ca: ACME.LetsEncrypt, |
| 224 | contact: ["mailto:ops@example.com"], |
| 225 | termsOfServiceAgreed: true, |
| 226 | }); |
| 227 | const certificate = yield* ACME.Certificate("Wildcard", { |
| 228 | account, |
| 229 | identifiers: ["*.example.com"], |
| 230 | solver: Cloudflare.DNS.AcmeSolver(zone), |
| 231 | }); |
| 232 | yield* Fly.Certificate("WildcardUpload", { |
| 233 | app: site, |
| 234 | hostname: "*.example.com", |
| 235 | kind: "custom", |
| 236 | fullchain: certificate.chain, |
| 237 | privateKey: certificate.privateKey, |
| 238 | }); |
| 239 | ``` |
| 240 | |
| 241 | The DNS solver publishes and removes the challenge TXT records. Cloudflare's |
| 242 | solver waits 60 seconds before validation so cached challenge values can expire. |
| 243 | Start with `ACME.LetsEncryptStaging` while developing; its certificates are not |
| 244 | trusted by browsers. |
| 245 | |
| 246 | Renewal is evaluated when you deploy, with a default threshold of 30 days before |
| 247 | expiry. Schedule deployments if you need automatic renewal. Account and |
| 248 | certificate private keys are persisted in stack state as redacted values; |
| 249 | protect the state backend as secret material. Redaction alone is not encryption. |
| 250 | |
| 251 | ## Manage certificates at runtime |
| 252 | |
| 253 | A Service can bind `Fly.WriteCertificates(Site)` to request, upload, inspect, |
| 254 | check, and remove certificates without redeploying. Provide |
| 255 | `Fly.WriteCertificatesHttp` on the Service Effect. For runtime issuance, |
| 256 | `ACME.IssueCertificate` and `ACME.IssueCertificateHttp` bind an existing account; |
| 257 | pass a runtime DNS solver to each issuance call. |
| 258 | |
| 259 | Runtime operations do not become stack resources. Your application owns renewal |
| 260 | scheduling, revocation, and removal of certificates it creates this way. Verify |
| 261 | that the chosen certificate authority is reachable from your runtime. |
| 262 | |
| 263 | Follow [Runtime issuance](/acme/runtime) for an authenticated Worker example, |
| 264 | [Using certificates](/acme/using-certificates) for Fly uploads, and |
| 265 | [Renewal & revocation](/acme/renewal) for scheduling and deletion policies. |
| 266 | The [`IssueCertificate`](/providers/acme/issuecertificate) and |
| 267 | [`WriteCertificates`](/providers/fly/writecertificates) references document the |
| 268 | binding methods. |
| 269 | |
| 270 | ## Where next |
| 271 | |
| 272 | The [tutorial](/fly/tutorial/part-1) deploys a public Service. |
| 273 | [Connect Services](/fly/compute/connecting-services) binds private |
| 274 | Services to each other on a stack network. |
| 275 | [Services](/fly/compute/services) covers private Services and grouping |
| 276 | Services in one [App](/fly/compute/apps). See the |
| 277 | [`IpAssignment`](/providers/fly/ipassignment) and |
| 278 | [`Certificate`](/providers/fly/certificate) references. |