diff --git a/.docker/vhost.conf b/.docker/vhost.conf deleted file mode 100644 index 860d5f3..0000000 --- a/.docker/vhost.conf +++ /dev/null @@ -1,15 +0,0 @@ -server { - listen 8080; - server_name localhost; - root /app/public; - - location / { - proxy_set_header X-Forwarded-For $remote_addr; - proxy_set_header Host $http_host; - proxy_pass http://node:3000; - } - - # Send log message to files symlinked to stdout/stderr. - error_log /dev/stderr; - access_log /dev/stdout; -} diff --git a/README.md b/README.md index 9034d5f..2552ffb 100644 --- a/README.md +++ b/README.md @@ -128,7 +128,7 @@ If that major version changes, update the pin in ## Test the data ```sh -open "http://$(docker compose port nginx 8080)" +open "http://$(docker compose port node 3000)" ``` ## Coding standards diff --git a/app.js b/app.js index 5ab3a70..9929b17 100644 --- a/app.js +++ b/app.js @@ -6,6 +6,11 @@ const sql = require('mssql') const app = express() +// Traefik is the only thing in front of the app and it sets X-Forwarded-For +// and X-Forwarded-Proto itself. Trust that one hop, so req.ip is the real +// client and req.protocol is https on the index page's links. +app.set('trust proxy', 1) + const config = require('./config') // One line per request: method, path, status, duration, row count, client ip. diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml index fa0a1a4..7760b4b 100644 --- a/docker-compose.dev.yml +++ b/docker-compose.dev.yml @@ -2,6 +2,6 @@ version: "3" services: - nginx: + node: labels: - "traefik.http.routers.${COMPOSE_PROJECT_NAME}.middlewares=ITKBasicAuth@file" diff --git a/docker-compose.redirect.yml b/docker-compose.redirect.yml index ee6568c..7f807e0 100644 --- a/docker-compose.redirect.yml +++ b/docker-compose.redirect.yml @@ -2,7 +2,7 @@ version: "3" services: - nginx: + node: labels: # Add www before domain and set redirect to non-www - "traefik.http.routers.www_${COMPOSE_PROJECT_NAME}-http.rule=Host(`www.${COMPOSE_SERVER_DOMAIN}`)" diff --git a/docker-compose.server.yml b/docker-compose.server.yml index b068384..fd51214 100644 --- a/docker-compose.server.yml +++ b/docker-compose.server.yml @@ -9,19 +9,20 @@ networks: internal: false services: - nginx: - image: nginxinc/nginx-unprivileged:alpine + node: + image: node:24 restart: unless-stopped + command: yarn start + working_dir: /app + environment: + - NODE_ENV=production networks: - app - frontend - depends_on: - - node ports: - - "8080" + - "3000" volumes: - - ${PWD}/.docker/vhost.conf:/etc/nginx/conf.d/default.conf:ro - - ./:/app:rw + - .:/app:delegated labels: - "traefik.enable=true" - "traefik.docker.network=frontend" @@ -31,17 +32,5 @@ services: - "traefik.http.middlewares.redirect-to-https.redirectscheme.scheme=https" - "traefik.http.routers.${COMPOSE_PROJECT_NAME}.rule=Host(`${COMPOSE_SERVER_DOMAIN}`)" - "traefik.http.routers.${COMPOSE_PROJECT_NAME}.entrypoints=websecure" - - node: - image: node:24 - restart: unless-stopped - command: yarn start - working_dir: /app - environment: - - NODE_ENV=production - networks: - - app - ports: - - "3000" - volumes: - - .:/app:delegated + # The node image has no EXPOSE, so traefik cannot guess the port. + - "traefik.http.services.${COMPOSE_PROJECT_NAME}.loadbalancer.server.port=3000" diff --git a/docker-compose.yml b/docker-compose.yml index 02972f6..6da27da 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -6,25 +6,6 @@ networks: internal: false services: - nginx: - image: nginxinc/nginx-unprivileged:alpine - networks: - - app - - frontend - depends_on: - - node - ports: - - "8080" - volumes: - - ${PWD}/.docker/vhost.conf:/etc/nginx/conf.d/default.conf:ro - - ./:/app:delegated - labels: - - "traefik.enable=true" - - "traefik.docker.network=frontend" - - "traefik.http.routers.${COMPOSE_PROJECT_NAME}.rule=Host(`${COMPOSE_DOMAIN}`)" - # - "traefik.http.routers.${COMPOSE_PROJECT_NAME}.middlewares=redirect-to-https" - # - "traefik.http.middlewares.redirect-to-https.redirectscheme.scheme=https" - mssql: # Opt-in: docker compose --profile db up -d # Default image is arm64-native so it runs unemulated on Apple Silicon. @@ -50,12 +31,21 @@ services: working_dir: /app networks: - app + - frontend ports: - "127.0.0.1::3000" environment: - NODE_ENV=development volumes: - .:/app:delegated + labels: + - "traefik.enable=true" + - "traefik.docker.network=frontend" + - "traefik.http.routers.${COMPOSE_PROJECT_NAME}.rule=Host(`${COMPOSE_DOMAIN}`)" + # The node image has no EXPOSE, so traefik cannot guess the port. + - "traefik.http.services.${COMPOSE_PROJECT_NAME}.loadbalancer.server.port=3000" + # - "traefik.http.routers.${COMPOSE_PROJECT_NAME}.middlewares=redirect-to-https" + # - "traefik.http.middlewares.redirect-to-https.redirectscheme.scheme=https" volumes: mssql-data: diff --git a/test.js b/test.js index ca2374a..eea9179 100644 --- a/test.js +++ b/test.js @@ -11,7 +11,7 @@ const assert = require('assert') const fs = require('fs') const path = require('path') -const BASE = process.env.BASE_URL || 'http://nginx:8080' +const BASE = process.env.BASE_URL || 'http://node:3000' const SEEDED_ROWS = 500 const checks = [] @@ -25,6 +25,14 @@ check('index lists both routes', async () => { assert.ok(body.posidryeartsl_old, 'posidryeartsl_old missing from index') }) +// Traefik terminates TLS and forwards X-Forwarded-Proto. Without +// app.set('trust proxy') express ignores it and the index emits http:// links +// on a page served over https. +check('the index honours the forwarded protocol', async () => { + const body = await (await fetch(`${BASE}/`, { headers: { 'x-forwarded-proto': 'https' } })).json() + assert.ok(body.posidryeartsl.csv.startsWith('https://'), `forwarded proto ignored: ${body.posidryeartsl.csv}`) +}) + check('csv has the expected columns', async () => { const res = await fetch(`${BASE}/posidryeartsl.csv`) assert.strictEqual(res.status, 200)