Skip to content

Custom domains, TCP & private networks

HTTP Services already get a generated *.up.railway.app hostname. That is enough for most apps. Add a CustomDomain for a hostname you control. Add a TcpProxy so a laptop or deploy-time migration can reach Postgres, MySQL, Mongo, or Redis. Named PrivateNetworks add custom DNS on the default *.railway.internal mesh.

A Service with port listens inside the container. Alchemy creates a *.up.railway.app domain and points it at that port.

export default class Api extends Railway.Service<Api>()(
"Api",
{
project: Site,
main: import.meta.url,
port: 3000,
},
Effect.gen(function* () {
return {
fetch: Effect.succeed(HttpServerResponse.text("hello")),
};
}),
) {}

Yield the Service in the Stack. api.url is https://{name}.up.railway.app.

export default Alchemy.Stack(
"MyApp",
{ providers: Railway.providers(), state: Alchemy.localState() },
Effect.gen(function* () {
const api = yield* Api;
return { url: api.url };
}),
);

There is no IpAssignment resource. Railway’s edge terminates TLS on 443.

A CustomDomain is a user hostname on a Service. Railway issues a Let’s Encrypt certificate once DNS is verified.

const site = yield* Railway.Project("Site");
const api = yield* Railway.Service("Api", {
project: site,
image: "hashicorp/http-echo",
port: 5678,
});
const www = yield* Railway.CustomDomain("Www", {
service: api,
environment: site,
domain: "www.example.com",
});

Yield Www in the Stack. Point DNS at www.verificationDnsHost / www.verificationToken. www.url is https://www.example.com. verified flips once Railway sees the record.

targetPort is optional and updates in place. Changing domain, service, or environment replaces the CustomDomain.

const www = yield* Railway.CustomDomain("Www", {
service: api,
environment: site,
domain: "www.example.com",
targetPort: 8080,
});

A TcpProxy exposes a service over public TCP ({domain}:{proxyPort} on *.proxy.rlwy.net). Identity is the Railway proxy id. There is no in-place update — changing service, postgres, redis, environment, or applicationPort replaces the proxy.

Railway.Postgres, MySQL, and Mongo already create one when public is true (the default). Use TcpProxy for Redis, or for a database created with public: false.

const site = yield* Railway.Project("Site");
const db = yield* Railway.Postgres("Db", { project: site, public: false });
const proxy = yield* Railway.TcpProxy("DbProxy", {
postgres: db,
environment: site,
applicationPort: 5432,
});

Redis on 6379:

const cache = yield* Railway.Redis("Cache", { project: site });
const proxy = yield* Railway.TcpProxy("CacheProxy", {
redis: cache,
environment: site,
applicationPort: 6379,
});

Pass a Railway.Service (or { serviceId }) for any other container.

In-service bindings (ConnectPostgres, ConnectMySQL, ConnectMongo, ReadWriteRedis) always use the private {name}.railway.internal URL. The TCP proxy is for laptops and CI.

Every environment already has the default *.railway.internal mesh — {serviceName}.railway.internal is derived from the Service name, no extra resource. PrivateNetwork create-or-gets an additional named network. Attach a Service with PrivateNetworkEndpoint for a custom DNS name.

const net = yield* Railway.PrivateNetwork("Mesh", {
environment: site,
});
const endpoint = yield* Railway.PrivateNetworkEndpoint("ApiDns", {
network: net,
service: api,
name: "api",
});

Railway has no per-network delete. Destroy is a no-op; the network is removed when its Project/Environment is deleted.

The tutorial deploys a Service that answers on *.up.railway.app. Custom domains and TCP proxies hang off the Service / Postgres / Redis. See the CustomDomain, TcpProxy, PrivateNetwork (includes PrivateNetworkEndpoint) references.