Add --host flag, listen on 127.0.0.1 by default

The server previously bound to all interfaces with no auth. Default to
loopback so it sits behind a reverse proxy; --host (or JOB_BOARD_HOST)
overrides it. Plain HOST is ignored since some environments set it to
the machine hostname.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-12 21:43:34 +02:00
co-authored by Claude Opus 5.5
parent beac32469c
commit 784c3c5220
3 changed files with 21 additions and 8 deletions
+8 -4
View File
@@ -18,14 +18,14 @@ No runtime dependencies. It uses Bun's built-in SQLite, HTTP server and HTMLRewr
```sh ```sh
bun install bun install
bun start # http://localhost:3000 bun start # http://127.0.0.1:3000
bun start --port 8080 # or -p 8080 bun start --port 8080 # or -p 8080
``` ```
The server scrapes every 24h. On startup it waits out the rest of the 24h since the last scrape (or scrapes right away if none happened in the last 24h). The server scrapes every 24h. On startup it waits out the rest of the 24h since the last scrape (or scrapes right away if none happened in the last 24h).
You can also click **Scrape now** in the UI, or run `bun run scrape` once (e.g. from cron). You can also click **Scrape now** in the UI, or run `bun run scrape` once (e.g. from cron).
Env vars: `PORT` (3000; `--port` overrides it), `SCRAPE_INTERVAL_HOURS` (24), `DB_PATH` (`data/jobs.sqlite`). Env vars: `PORT` (3000; `--port` overrides it), `JOB_BOARD_HOST` (127.0.0.1; `--host` overrides it), `SCRAPE_INTERVAL_HOURS` (24), `DB_PATH` (`data/jobs.sqlite`).
## Build a binary for a server ## Build a binary for a server
@@ -41,11 +41,15 @@ bun run build:linux-arm64 # bin/job-board-linux-arm64 (e.g. Raspberry Pi, ARM
On the server: On the server:
```sh ```sh
./job-board # web server on :3000, scrapes every 24h ./job-board # http://127.0.0.1:3000, scrapes every 24h
./job-board --port 8080 ./job-board --port 8080
./job-board --host 0.0.0.0 --port 8080 # listen on all interfaces
./job-board scrape # scrape once and exit (exit code 1 if a scraper failed) ./job-board scrape # scrape once and exit (exit code 1 if a scraper failed)
``` ```
It listens on loopback (`127.0.0.1`) only by default, so it's meant to sit behind a reverse proxy
(nginx, Caddy). There is no login: only use `--host 0.0.0.0` on a trusted network.
The database goes to `data/jobs.sqlite` next to the binary (override with `DB_PATH`). The database goes to `data/jobs.sqlite` next to the binary (override with `DB_PATH`).
The binaries target glibc Linux; for Alpine use `--target=bun-linux-x64-musl`, and for old CPUs The binaries target glibc Linux; for Alpine use `--target=bun-linux-x64-musl`, and for old CPUs
without AVX2 use `--target=bun-linux-x64-baseline`. without AVX2 use `--target=bun-linux-x64-baseline`.
@@ -58,7 +62,7 @@ Description=Job board
After=network-online.target After=network-online.target
[Service] [Service]
ExecStart=/opt/job-board/job-board --port 3000 ExecStart=/opt/job-board/job-board --host 127.0.0.1 --port 3000
Restart=on-failure Restart=on-failure
User=jobboard User=jobboard
+6 -2
View File
@@ -1,10 +1,10 @@
// Entry point (also used by the compiled binary): // Entry point (also used by the compiled binary):
// job-board [serve] [--port <n>] start the web server (scrapes every 24h) // job-board [serve] [--host <addr>] [--port <n>] start the web server (scrapes every 24h)
// job-board scrape run all scrapers once and exit (exit code 1 if any failed) // job-board scrape run all scrapers once and exit (exit code 1 if any failed)
export {}; export {};
const USAGE = "Usage: job-board [serve] [--port <n>] | job-board scrape"; const USAGE = "Usage: job-board [serve] [--host <addr>] [--port <n>] | job-board scrape\n --host address to listen on (default 127.0.0.1; 0.0.0.0 = all interfaces)\n --port port to listen on (default 3000)";
function fail(message: string): never { function fail(message: string): never {
console.error(`${message}\n${USAGE}`); console.error(`${message}\n${USAGE}`);
@@ -20,6 +20,10 @@ for (let i = 0; i < args.length; i++) {
const port = Number(value); const port = Number(value);
if (!Number.isInteger(port) || port < 1 || port > 65535) fail(`Invalid port "${value ?? ""}".`); if (!Number.isInteger(port) || port < 1 || port > 65535) fail(`Invalid port "${value ?? ""}".`);
process.env.PORT = String(port); // read by server.ts; overrides the PORT env var process.env.PORT = String(port); // read by server.ts; overrides the PORT env var
} else if (arg === "--host" || arg.startsWith("--host=")) {
const value = arg.startsWith("--host=") ? arg.slice("--host=".length) : args[++i];
if (!value || value.startsWith("-")) fail(`Invalid host "${value ?? ""}".`);
process.env.JOB_BOARD_HOST = value; // read by server.ts; overrides the env var
} else if (arg === "--help" || arg === "-h") { } else if (arg === "--help" || arg === "-h") {
console.log(USAGE); console.log(USAGE);
process.exit(0); process.exit(0);
+6 -1
View File
@@ -3,6 +3,9 @@ import { isScraping, runAllScrapers } from "./scrape";
import { scrapers } from "./scrapers"; import { scrapers } from "./scrapers";
const PORT = Number(process.env.PORT ?? 3000); const PORT = Number(process.env.PORT ?? 3000);
// Loopback only by default; use --host 0.0.0.0 (or JOB_BOARD_HOST) to expose it on the network.
// Not plain HOST: some shells/environments set that to the machine hostname.
const HOST = process.env.JOB_BOARD_HOST || "127.0.0.1";
const SCRAPE_INTERVAL_HOURS = Number(process.env.SCRAPE_INTERVAL_HOURS ?? 24); const SCRAPE_INTERVAL_HOURS = Number(process.env.SCRAPE_INTERVAL_HOURS ?? 24);
// --- Scheduler: scrape every SCRAPE_INTERVAL_HOURS. On startup, wait out whatever is left of the // --- Scheduler: scrape every SCRAPE_INTERVAL_HOURS. On startup, wait out whatever is left of the
@@ -205,6 +208,7 @@ function page(url: URL) {
const html = (body: string) => new Response(body, { headers: { "content-type": "text/html; charset=utf-8" } }); const html = (body: string) => new Response(body, { headers: { "content-type": "text/html; charset=utf-8" } });
Bun.serve({ Bun.serve({
hostname: HOST,
port: PORT, port: PORT,
routes: { routes: {
"/": (req) => html(page(new URL(req.url))), "/": (req) => html(page(new URL(req.url))),
@@ -227,4 +231,5 @@ Bun.serve({
fetch: () => new Response("Not found", { status: 404 }), fetch: () => new Response("Not found", { status: 404 }),
}); });
console.log(`Job board running at http://localhost:${PORT} (scraping every ${SCRAPE_INTERVAL_HOURS}h)`); const shownHost = HOST.includes(":") ? `[${HOST}]` : HOST;
console.log(`Job board running at http://${shownHost}:${PORT} (scraping every ${SCRAPE_INTERVAL_HOURS}h)`);