Skip to content

Commit 96c07ce

Browse files
committed
fix: bind to 0.0.0.0 and prefer PORT over APP_PORT for platform deploys
The Express and H3 drivers now read APP_HOST (falling back to HOST), defaulting to 0.0.0.0 so the server listens on all network interfaces. This lets platform healthcheck proxies such as Railway reach the app instead of failing against a localhost-only bind. Server port resolution also now prefers the platform-provided PORT env variable over APP_PORT (PORT -> APP_PORT -> 3000), so deployments bind to the expected port without extra configuration. Adds APP_HOST to the Express/H3 .env templates and documents host/port binding (with Railway guidance) in the runtime guides.
1 parent 31daa56 commit 96c07ce

8 files changed

Lines changed: 94 additions & 16 deletions

File tree

docs/guide/express-runtime.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,22 @@ this.driver = new ExpressDriver({
121121
});
122122
```
123123

124+
## Host & Port Binding
125+
126+
The server resolves its listening port and host from environment variables, which makes it portable across hosting platforms (Railway, Heroku, Render, etc.).
127+
128+
| Variable | Default | Purpose |
129+
| ---------------------- | --------- | --------------------------------------------------------------- |
130+
| `PORT` || Platform-provided port. Preferred over `APP_PORT` when present. |
131+
| `APP_PORT` | `3000` | Application port used when `PORT` is not set. |
132+
| `APP_HOST` (or `HOST`) | `0.0.0.0` | Host the server binds to. |
133+
134+
Port resolution order is `PORT``APP_PORT``3000`. Platforms such as Railway inject a `PORT` variable at runtime, so it takes precedence automatically.
135+
136+
The server binds to `0.0.0.0` by default so it is reachable on all network interfaces. This is required for platform healthcheck proxies (e.g. Railway) to reach the app. To restrict the server to local connections only, set `APP_HOST=localhost`.
137+
138+
> **Deploying to Railway:** point `APP_PORT` at Railway's `PORT` variable (`APP_PORT=${{ PORT }}`) or simply rely on the built-in `PORT` precedence, and leave `APP_HOST` at its `0.0.0.0` default.
139+
124140
## Notes
125141

126142
- `app.boot(port)` mounts public assets, binds router, applies middleware, registers error handling, starts the server, and attaches graceful shutdown.

docs/guide/h3-runtime.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,22 @@ this.driver = new H3Driver({
118118
});
119119
```
120120

121+
## Host & Port Binding
122+
123+
The server resolves its listening port and host from environment variables, which makes it portable across hosting platforms (Railway, Heroku, Render, etc.).
124+
125+
| Variable | Default | Purpose |
126+
| ---------------------- | --------- | --------------------------------------------------------------- |
127+
| `PORT` || Platform-provided port. Preferred over `APP_PORT` when present. |
128+
| `APP_PORT` | `3000` | Application port used when `PORT` is not set. |
129+
| `APP_HOST` (or `HOST`) | `0.0.0.0` | Host the server binds to. |
130+
131+
Port resolution order is `PORT``APP_PORT``3000`. Platforms such as Railway inject a `PORT` variable at runtime, so it takes precedence automatically.
132+
133+
The server binds to `0.0.0.0` by default so it is reachable on all network interfaces. This is required for platform healthcheck proxies (e.g. Railway) to reach the app. To restrict the server to local connections only, set `APP_HOST=localhost`.
134+
135+
> **Deploying to Railway:** point `APP_PORT` at Railway's `PORT` variable (`APP_PORT=${{ PORT }}`) or simply rely on the built-in `PORT` precedence, and leave `APP_HOST` at its `0.0.0.0` default.
136+
121137
## Notes
122138

123139
- `app.boot(port)` mounts public assets, binds router, applies middleware, starts the server, and attaches graceful shutdown.

docs/more/changelog.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,20 @@ The format follows semantic versioning principles.
88

99
### Added
1010

11+
- Added `APP_HOST` (with `HOST` fallback) to override the server bind host in the Express and H3 drivers, defaulting to `0.0.0.0` so the app is reachable on all network interfaces.
12+
1113
### Changed
1214

15+
- Prefer the platform-provided `PORT` env variable over `APP_PORT` when resolving the server port, so deployments on Railway, Heroku, and similar platforms bind to the expected port automatically.
16+
1317
### Docs
1418

19+
- Documented host and port binding for the Express and H3 runtimes, including Railway deployment guidance.
20+
1521
### Fixed
1622

23+
- Fixed deployments where the server bound to `localhost` and platform healthcheck proxies (e.g. Railway) could not reach the app.
24+
1725
## [0.4.0] - 2026-05-07
1826

1927
### Added

packages/contract/src/Arkstack.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -53,14 +53,15 @@ export abstract class Arkstack<TApp, TRoutes = unknown, THandler = unknown> {
5353
/**
5454
* Boostrap the app and start up the server
5555
*
56-
* @param defaultPort start the server with this port if none is APP_PORT env variable is not set
56+
* @param defaultPort start the server with this port if neither PORT nor APP_PORT env variable is set
5757
* @param defer Set to true to skip server startup
5858
*/
5959
async startup (defaultPort: number = 3000, defer?: boolean) {
6060
const { bootWithDetectedPort } = await import('@arkstack/common')
61+
// Prefer the platform-provided PORT (e.g. Railway, Heroku) over APP_PORT when available.
6162
await bootWithDetectedPort<TApp, TRoutes, THandler>(async (port) => {
6263
await this.boot(port, defer)
63-
}, Number(process.env.APP_PORT ?? defaultPort), this as never, defer)
64+
}, Number(process.env.PORT ?? process.env.APP_PORT ?? defaultPort), this as never, defer)
6465
}
6566

6667
/**

packages/driver-express/src/index.ts

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import express, { type ErrorRequestHandler, type Express, type Handler } from 'express'
22

33
import { ArkstackKitDriver, PromiseOrValue } from '@arkstack/contract'
4-
import { Logger } from '@arkstack/common'
4+
import { Logger, env } from '@arkstack/common'
55
import { defaultErrorHandler } from './error-handler'
66
import { Middleware, MiddlewareConfig } from './types'
77
import { resolveMiddleware } from '@arkstack/http'
@@ -118,15 +118,22 @@ export class ExpressDriver extends ArkstackKitDriver<Express, Handler> {
118118

119119
/**
120120
* Starts the Express server on the specified port.
121-
*
122-
* @param app
123-
* @param port
121+
*
122+
* The bind host can be overridden with the `APP_HOST` (or `HOST`) env
123+
* variable. It defaults to `0.0.0.0` so the server is reachable on all
124+
* network interfaces, which platforms like Railway require for their
125+
* healthcheck proxy to reach the app.
126+
*
127+
* @param app
128+
* @param port
124129
*/
125130
start (app: Express, port: number): void {
126-
app.listen(port, () => {
131+
const host = env('APP_HOST', env('HOST', '0.0.0.0'))
132+
133+
app.listen(port, host, () => {
127134
Logger.log([
128135
['Server is running on', 'white'],
129-
[`http://localhost:${port}`, 'cyan']
136+
[`http://${host}:${port}`, 'cyan']
130137
], ' ')
131138
})
132139
}

packages/driver-h3/src/index.ts

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { H3, H3Event, serve, toResponse } from 'h3'
33
import { Middleware, MiddlewareConfig } from './types'
44

55
import { Middleware as H3BaseMiddleware } from 'clear-router/types/h3'
6-
import { Logger } from '@arkstack/common'
6+
import { Logger, env } from '@arkstack/common'
77
import { defaultErrorHandler } from './error-handler'
88
import { resolveMiddleware } from '@arkstack/http'
99
import { staticAssetHandler } from './middlewares'
@@ -126,15 +126,22 @@ export class H3Driver extends ArkstackKitDriver<H3, H3Middleware> {
126126

127127
/**
128128
* Starts the H3 server on the specified port.
129-
*
130-
* @param app
131-
* @param port
129+
*
130+
* The bind host can be overridden with the `APP_HOST` (or `HOST`) env
131+
* variable. It defaults to `0.0.0.0` so the server is reachable on all
132+
* network interfaces, which platforms like Railway require for their
133+
* healthcheck proxy to reach the app.
134+
*
135+
* @param app
136+
* @param port
132137
*/
133138
start (app: H3, port: number): void {
134-
serve(app, { port, silent: true }).ready().then(() => {
139+
const host = env('APP_HOST', env('HOST', '0.0.0.0'))
140+
141+
serve(app, { port, hostname: host, silent: true }).ready().then(() => {
135142
Logger.log([
136143
['Server is running on', 'white'],
137-
[`http://localhost:${port}`, 'cyan']
144+
[`http://${host}:${port}`, 'cyan']
138145
], ' ')
139146
})
140147
}

templates/express/.env.example

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
APP_NAME="Arkstack"
22
APP_URL="http://localhost:3000"
33
APP_PORT=3000
4+
# Host the server binds to. Defaults to 0.0.0.0 (all interfaces).
5+
# Set to localhost to restrict access to the local machine only.
6+
APP_HOST=0.0.0.0
7+
FILESYSTEM_DISK="local"
48

59
TWO_FACTOR_ENCRYPTION_KEY="SRNysvjD139E6hrZfS8If7pQi0Uv/2ZBXb6fAT+agDZNpbfIAnVyHHvholN9c74v"
610
JWT_SECRET="your-jwt-secret"
@@ -14,4 +18,11 @@ MAIL_SECURE=false
1418
MAIL_USERNAME="no-reply@example.com"
1519
MAIL_PASSWORD="password"
1620
MAIL_FROM_ADDRESS="no-reply@example.com"
17-
MAIL_TEST_ADDRESS=""
21+
MAIL_TEST_ADDRESS=
22+
23+
AWS_ACCESS_KEY_ID=
24+
AWS_SECRET_ACCESS_KEY=
25+
AWS_DEFAULT_REGION=
26+
AWS_BUCKET=
27+
AWS_URL=
28+
AWS_ENDPOINT=

templates/h3/.env.example

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,16 @@
11
APP_NAME="Arkstack"
22
APP_URL="http://localhost:3000"
33
APP_PORT=3000
4+
# Host the server binds to. Defaults to 0.0.0.0 (all interfaces).
5+
# Set to localhost to restrict access to the local machine only.
6+
APP_HOST=0.0.0.0
7+
FILESYSTEM_DISK="local"
48

59
TWO_FACTOR_ENCRYPTION_KEY="SRNysvjD139E6hrZfS8If7pQi0Uv/2ZBXb6fAT+agDZNpbfIAnVyHHvholN9c74v"
610
JWT_SECRET="your-jwt-secret"
711
JWT_EXPIRES_IN="1h"
812

13+
914
DATABASE_URL="postgres://postgres:postgres@localhost:5432/arkstark_test?schema=public"
1015

1116
MAIL_HOST="localhost"
@@ -14,4 +19,11 @@ MAIL_SECURE=false
1419
MAIL_USERNAME="no-reply@example.com"
1520
MAIL_PASSWORD="password"
1621
MAIL_FROM_ADDRESS="no-reply@example.com"
17-
MAIL_TEST_ADDRESS=""
22+
MAIL_TEST_ADDRESS=
23+
24+
AWS_ACCESS_KEY_ID=
25+
AWS_SECRET_ACCESS_KEY=
26+
AWS_DEFAULT_REGION=
27+
AWS_BUCKET=
28+
AWS_URL=
29+
AWS_ENDPOINT=

0 commit comments

Comments
 (0)