diff --git a/README.md b/README.md index f055f6f39..f19e7cdf6 100644 --- a/README.md +++ b/README.md @@ -89,7 +89,38 @@ curl -O https://raw.githubusercontent.com/bluewave-labs/checkmate/master/docker/ JWT_SECRET="$(openssl rand -hex 32)" docker compose up -d ``` -Then open http://localhost:52345. If the app is reached at another origin (domain or LAN IP), set `CLIENT_HOST` accordingly. To build the image yourself, run `docker build -f docker/Dockerfile -t checkmate .` from a checkout. For TLS, put any reverse proxy (Caddy, Traefik, nginx) in front of port 52345. +Then open http://localhost:52345. If the app is reached at another origin (domain or LAN IP), set `CLIENT_HOST` accordingly. To build the image yourself, run `docker build -f docker/Dockerfile -t checkmate .` from a checkout. + +### VPS deployment with HTTPS (Caddy) + +For a public VPS, use the dedicated Compose file below. It runs the same all-in-one Checkmate image and MongoDB, adds Caddy for automatic HTTPS, and deliberately does **not** publish Checkmate's internal port `52345` to the host. + +Before starting, point an A/AAAA record for your domain at the VPS and allow inbound TCP ports `80` and `443`. Then download the two VPS deployment files into an empty directory: + +```bash +mkdir checkmate && cd checkmate +curl -O https://raw.githubusercontent.com/bluewave-labs/checkmate/master/docker/docker-compose.vps.yaml +curl -O https://raw.githubusercontent.com/bluewave-labs/checkmate/master/docker/Caddyfile +``` + +Create `.env`, replacing `checkmate.example.com` with your domain: + +```bash +export CHECKMATE_DOMAIN=checkmate.example.com +export CLIENT_HOST="https://${CHECKMATE_DOMAIN}" +export JWT_SECRET="$(openssl rand -hex 32)" +printf 'CHECKMATE_DOMAIN=%s\nCLIENT_HOST=%s\nJWT_SECRET=%s\n' \ + "$CHECKMATE_DOMAIN" "$CLIENT_HOST" "$JWT_SECRET" > .env +``` + +Start the stack and check its status: + +```bash +docker compose -f docker-compose.vps.yaml up -d +docker compose -f docker-compose.vps.yaml ps +``` + +Open `https://checkmate.example.com`, replacing the example domain with yours. Caddy obtains and renews the TLS certificate automatically. Keep the generated `.env` private: `JWT_SECRET` signs login tokens and must not be committed or shared. ### Configuration @@ -110,7 +141,7 @@ The web client needs no configuration by default: it calls the API on the same o | `CLIENT_CONFIG_CLIENT_HOST` | Origin used when the client builds absolute links (invites, status pages); defaults to the browser's current origin | | `CLIENT_CONFIG_LOG_LEVEL` | Browser console log level: `error`, `warn`, `info`, or `debug` (default `error`) | -> **Upgrading from an older image?** The `UPTIME_APP_*` variables (`UPTIME_APP_API_BASE_URL`, `UPTIME_APP_CLIENT_HOST`, `UPTIME_APP_LOG_LEVEL`) are no longer read. In most setups no replacement is needed — the same-origin defaults cover them; if you pointed the client at a different origin, use the `CLIENT_CONFIG_*` equivalents above. The `checkmate-client`, `checkmate-backend`, `checkmate-mongo`, and `checkmate-backend-mono-multiarch` images are no longer updated — switch to `ghcr.io/bluewave-labs/checkmate`, keeping your existing MongoDB service and data volume. +> **Upgrading from an older image?** The `UPTIME_APP_*` variables (`UPTIME_APP_API_BASE_URL`, `UPTIME_APP_CLIENT_HOST`, `UPTIME_APP_LOG_LEVEL`) are no longer read. In most deployments no replacement is needed — the same-origin defaults cover them, including the Caddy example above. If you intentionally serve the API from another origin, use the `CLIENT_CONFIG_*` equivalents. The `checkmate-client`, `checkmate-backend`, `checkmate-mongo`, and `checkmate-backend-mono-multiarch` images are no longer updated — switch to `ghcr.io/bluewave-labs/checkmate`, keeping your existing MongoDB service and data volume. See full installation instructions in the [Checkmate documentation portal](https://checkmate.so/docs). @@ -228,4 +259,3 @@ Here's how you can contribute: [![Star History Chart](https://api.star-history.com/svg?repos=bluewave-labs/checkmate&type=Date)](https://star-history.com/#bluewave-labs/Checkmate&Date) - diff --git a/docker/Caddyfile b/docker/Caddyfile new file mode 100644 index 000000000..3b36e1402 --- /dev/null +++ b/docker/Caddyfile @@ -0,0 +1,3 @@ +{$CHECKMATE_DOMAIN} { + reverse_proxy checkmate:52345 +} diff --git a/docker/docker-compose.vps.yaml b/docker/docker-compose.vps.yaml new file mode 100644 index 000000000..8a0365677 --- /dev/null +++ b/docker/docker-compose.vps.yaml @@ -0,0 +1,63 @@ +services: + checkmate: + image: ghcr.io/bluewave-labs/checkmate:latest + pull_policy: always + restart: always + environment: + - DB_CONNECTION_STRING=mongodb://mongodb:27017/uptime_db + - CLIENT_HOST=${CLIENT_HOST:?set CLIENT_HOST in your environment} + - JWT_SECRET=${JWT_SECRET:?set JWT_SECRET in your environment} + - NODE_ENV=production + - TRUST_PROXY=true + stop_grace_period: 60s + healthcheck: + test: + [ + "CMD", + "node", + "-e", + "require('http').get('http://localhost:52346/livez',r=>process.exit(r.statusCode===200?0:1)).on('error',()=>process.exit(1))", + ] + interval: 15s + timeout: 3s + start_period: 60s + start_interval: 2s + retries: 3 + depends_on: + mongodb: + condition: service_healthy + + mongodb: + image: mongo:8.0 + restart: always + command: ["mongod", "--quiet", "--bind_ip_all"] + volumes: + - mongo-data:/data/db + healthcheck: + test: ["CMD", "mongosh", "--eval", "db.adminCommand('ping')", "--quiet"] + interval: 5s + timeout: 30s + start_period: 0s + start_interval: 1s + retries: 30 + + caddy: + image: caddy:2 + restart: always + ports: + - "80:80" + - "443:443" + environment: + - CHECKMATE_DOMAIN=${CHECKMATE_DOMAIN:?set CHECKMATE_DOMAIN in your environment} + volumes: + - ./Caddyfile:/etc/caddy/Caddyfile:ro + - caddy-data:/data + - caddy-config:/config + depends_on: + checkmate: + condition: service_healthy + +volumes: + mongo-data: + caddy-data: + caddy-config: diff --git a/server/src/app.ts b/server/src/app.ts index 12e552406..c6654a3c5 100644 --- a/server/src/app.ts +++ b/server/src/app.ts @@ -31,6 +31,7 @@ export const createApp = ({ }) => { const allowedOrigin = envSettings.clientHost; const app = express(); + app.set("trust proxy", envSettings.trustProxy); const defaultCorsOptions: CorsOptions = { origin: allowedOrigin, methods: "GET,HEAD,PUT,PATCH,POST,DELETE,OPTIONS", diff --git a/server/src/config/envValidation.ts b/server/src/config/envValidation.ts index a769a28c7..e9a595634 100644 --- a/server/src/config/envValidation.ts +++ b/server/src/config/envValidation.ts @@ -36,6 +36,7 @@ const envSchema = z.object({ // Feature flags STATUS_PAGE_THEMES_ENABLED: booleanCoercion.default(true), + TRUST_PROXY: booleanCoercion.default(false), }); export type ValidatedEnv = z.infer; diff --git a/server/src/domain/app-settings/app-settings.service.ts b/server/src/domain/app-settings/app-settings.service.ts index 2df2b7512..e0265b2c1 100755 --- a/server/src/domain/app-settings/app-settings.service.ts +++ b/server/src/domain/app-settings/app-settings.service.ts @@ -16,6 +16,7 @@ export type EnvConfig = { queueMode: QueueMode; queuePrimaryProcesses: boolean; statusPageThemesEnabled: boolean; + trustProxy: boolean; clientConfig: ClientRuntimeConfig; }; @@ -43,6 +44,7 @@ export class SettingsService implements ISettingsService { queueMode: env.QUEUE_MODE, queuePrimaryProcesses: env.QUEUE_PRIMARY_PROCESSES, statusPageThemesEnabled: env.STATUS_PAGE_THEMES_ENABLED, + trustProxy: env.TRUST_PROXY, clientConfig: { ...(env.CLIENT_CONFIG_API_BASE_URL && { apiBaseUrl: env.CLIENT_CONFIG_API_BASE_URL }), ...(env.CLIENT_CONFIG_CLIENT_HOST && { clientHost: env.CLIENT_CONFIG_CLIENT_HOST }), diff --git a/server/test/unit/services/settingsService.test.ts b/server/test/unit/services/settingsService.test.ts index 063a64f6d..932220fd7 100644 --- a/server/test/unit/services/settingsService.test.ts +++ b/server/test/unit/services/settingsService.test.ts @@ -15,7 +15,10 @@ const makeEnv = (overrides?: Partial): ValidatedEnv => CLIENT_HOST: "http://localhost:5173", DB_CONNECTION_STRING: "mongodb://localhost:27017/test_db", DB_TYPE: "mongodb", + QUEUE_MODE: "primary", + QUEUE_PRIMARY_PROCESSES: true, STATUS_PAGE_THEMES_ENABLED: false, + TRUST_PROXY: false, ...overrides, }) as ValidatedEnv; @@ -71,7 +74,10 @@ describe("SettingsService", () => { clientHost: "http://localhost:5173", dbConnectionString: "mongodb://localhost:27017/test_db", dbType: "mongodb", + queueMode: "primary", + queuePrimaryProcesses: true, statusPageThemesEnabled: false, + trustProxy: false, clientConfig: {}, }); }); @@ -89,11 +95,12 @@ describe("SettingsService", () => { }); it("reflects custom env values", () => { - const { service } = createService({ NODE_ENV: "production", LOG_LEVEL: "error" }); + const { service } = createService({ NODE_ENV: "production", LOG_LEVEL: "error", TRUST_PROXY: true }); const settings = service.getSettings(); expect(settings.nodeEnv).toBe("production"); expect(settings.logLevel).toBe("error"); + expect(settings.trustProxy).toBe(true); }); });