Optimizations for the Website service (#9)
This PR contains the work done to provide optimizations to the current service, such as a health-check endpoint, pre-renders static HTML pages, and hardens the error page's CSP. To provide further details about the work: * Added the `HealthController` controller serving GET `/health` with a static JSON payload. * Added the `CachedHTMLResponse` response, which renders a static HTMLDocument to bytes once and reuses them per request (no Content-Length, so responses stay compressible). * Integrated the response into the `RootController` and the `NotFoundMiddleware` middleware to avoid re-rendering on hot paths. * Added a `RouterMethods.addRoutes(_:)` extension and switched the router in App+build to use it. * Moved the inline style from the `ErrorPage` page into a dedicated style file so the CSP needs no inline-style escape hatch. * Fixed the `IndexPage` page path inconsistencies. * Written the `README` file. Reviewed-on: rock-n-code/loud-amsterdam#9 Co-authored-by: Javier Cicchelli <javier@rock-n-code.com> Co-committed-by: Javier Cicchelli <javier@rock-n-code.com>
This commit is contained in:
+136
-2
@@ -1,2 +1,136 @@
|
||||
# Website
|
||||
Hummingbird server framework project
|
||||
# Loud Website
|
||||
The **Loud** public website service — a [Hummingbird](https://github.com/hummingbird-project/hummingbird) server that renders a static landing page and serves the site's static assets.
|
||||
|
||||
## Overview
|
||||
The service:
|
||||
- Serves the landing page at `GET /` (rendered once with [Elementary](https://github.com/elementary-swift/elementary) and cached).
|
||||
- Serves static files (CSS, JS, icons, manifest, `robots.txt`) from `Resources/Static` via Hummingbird's `FileMiddleware`, tagged with media-type-specific `Cache-Control`.
|
||||
- Returns a custom HTML 404 page for any request that matches neither a route nor a static file.
|
||||
- Compresses responses (gzip/deflate) above a configurable size when the client advertises support.
|
||||
- Stamps a hardened set of security headers on every response.
|
||||
|
||||
## Requirements
|
||||
- Swift 6.3 toolchain (`swift-tools-version:6.3`).
|
||||
- Docker (optional) for the containerized run/deploy workflow.
|
||||
|
||||
## Architecture
|
||||
Two SwiftPM targets:
|
||||
| Target | Kind | Path | Role |
|
||||
| --- | --- | --- | --- |
|
||||
| `Website` | executable | `Sources/App` | Entry point: reads configuration, builds and runs the application. |
|
||||
| `WebsiteCore` | library | `Sources/Library` | Controllers, middlewares, pages, cached responses, and configuration helpers. |
|
||||
|
||||
Requests pass through the middleware chain in this order (outermost first), then reach the routes:
|
||||
```
|
||||
LogRequestsMiddleware
|
||||
→ SecurityHeadersMiddleware (security headers on every response)
|
||||
→ ResponseCompressionMiddleware (gzip/deflate above the size threshold)
|
||||
→ NotFoundMiddleware (renders the 404 page on .notFound)
|
||||
→ FileMiddleware (serves Resources/Static)
|
||||
RootController (GET / → landing page)
|
||||
```
|
||||
|
||||
## Configuration
|
||||
Configuration is read through [swift-configuration](https://github.com/apple/swift-configuration) from
|
||||
the following sources, **highest precedence first**:
|
||||
1. Command-line arguments (e.g. `--http-host 0.0.0.0`)
|
||||
2. Process environment variables
|
||||
3. A `.env` file in the working directory (optional)
|
||||
4. Built-in defaults
|
||||
|
||||
### Environment variable naming
|
||||
A dotted config key maps to an environment variable by upper-casing, splitting camelCase, and replacing separators with `_`. For example `http.serverName` → `HTTP_SERVER_NAME`,
|
||||
`security.strictTransportSecurity` → `SECURITY_STRICT_TRANSPORT_SECURITY`, `cache.maxAge.text` →` CACHE_MAX_AGE_TEXT`.
|
||||
> To disable a header or override a value, leave the variable **unset** to fall back to the default. A
|
||||
> variable that is set but **blank** is treated as an explicit empty value, not as "use the default".
|
||||
|
||||
### Static file caching
|
||||
| Config key | Environment variable | Default | Description |
|
||||
| --- | --- | --- | --- |
|
||||
| `cache.maxAge.text` | `CACHE_MAX_AGE_TEXT` | `3600` (1 hour) | `max-age` for text assets (CSS, JS, plain text); also marked `must-revalidate`. |
|
||||
| `cache.maxAge.image` | `CACHE_MAX_AGE_IMAGE` | `604800` (1 week) | `max-age` for images (ICO, PNG, SVG). |
|
||||
| `cache.maxAge.default` | `CACHE_MAX_AGE_DEFAULT` | `86400` (1 day) | `max-age` for everything else (e.g. the web manifest). |
|
||||
|
||||
### Response compression
|
||||
| Config key | Environment variable | Default | Description |
|
||||
| --- | --- | --- | --- |
|
||||
| `compression.minimumResponseSize` | `COMPRESSION_MINIMUM_RESPONSE_SIZE` | `1024` | Minimum response body size, in bytes, before compression is applied. |
|
||||
|
||||
### HTTP server
|
||||
| Config key | Environment variable | Default | Description |
|
||||
| --- | --- | --- | --- |
|
||||
| `http.host` | `HTTP_HOST` | _none_ | Host the server binds to. Supplied via the `--http-host` CLI flag (the Docker image passes `0.0.0.0`). |
|
||||
| `http.port` | `HTTP_PORT` | _none_ | Port the server listens on. Supplied via the `--http-port` CLI flag (the Docker image passes `8080`). |
|
||||
| `http.serverName` | `HTTP_SERVER_NAME` | `LoudWebsite` | Server name and logger label. |
|
||||
|
||||
### Logging
|
||||
| Config key | Environment variable | Default | Description |
|
||||
| --- | --- | --- | --- |
|
||||
| `log.level` | `LOG_LEVEL` | `info` | Minimum log level. |
|
||||
|
||||
### Paths
|
||||
| Config key | Environment variable | Default | Description |
|
||||
| --- | --- | --- | --- |
|
||||
| `path.staticFiles` | `PATH_STATIC_FILES` | `Resources/Static` | Directory, relative to the working directory, that static files are served from. |
|
||||
|
||||
### Security headers
|
||||
| Config key | Environment variable | Default |
|
||||
| --- | --- | --- |
|
||||
| `security.contentSecurityPolicy` | `SECURITY_CONTENT_SECURITY_POLICY` | `default-src 'self'; object-src 'none'; base-uri 'self'; frame-ancestors 'none'` |
|
||||
| `security.contentTypeOptions` | `SECURITY_CONTENT_TYPE_OPTIONS` | `nosniff` |
|
||||
| `security.frameOptions` | `SECURITY_FRAME_OPTIONS` | `DENY` |
|
||||
| `security.referrerPolicy` | `SECURITY_REFERRER_POLICY` | `strict-origin-when-cross-origin` |
|
||||
| `security.permissionsPolicy` | `SECURITY_PERMISSIONS_POLICY` | `accelerometer=(), camera=(), geolocation=(), gyroscope=(), magnetometer=(), microphone=(), payment=(), usb=()` |
|
||||
| `security.strictTransportSecurity` | `SECURITY_STRICT_TRANSPORT_SECURITY` | _none (omitted)_ |
|
||||
|
||||
`Strict-Transport-Security` has **no default** and is omitted unless explicitly configured: it only takes effect over HTTPS (browsers ignore it on plain HTTP) and is "sticky" in browsers, so it must stay off in local HTTP development. It is enabled for production in `docker-compose.yml`, where it only has an effect once traffic is served over HTTPS behind a TLS-terminating proxy.
|
||||
|
||||
## Running locally
|
||||
Directly with Swift:
|
||||
```sh
|
||||
swift run Website # binds to Hummingbird's default 127.0.0.1:8080
|
||||
swift run Website --http-host 0.0.0.0 --http-port 9000 --log-level debug
|
||||
```
|
||||
|
||||
Or via the Makefile / Docker (uses `docker-compose.override.yml`, which builds from source and sets`LOG_LEVEL=debug`:
|
||||
```sh
|
||||
make pkg-build # swift build
|
||||
make img-mount # docker compose up --build --detach
|
||||
make img-unmount # docker compose down + remove the local image
|
||||
```
|
||||
|
||||
`make help` lists every available target.
|
||||
## Testing
|
||||
```sh
|
||||
make pkg-test
|
||||
# = swift test --disable-xctest --enable-code-coverage --enable-swift-testing --parallel
|
||||
```
|
||||
|
||||
Tests use the [Swift Testing](https://developer.apple.com/documentation/testing/) framework. The`Website.xctestplan` covers two targets: `WebsiteTests` (the executable/integration tests) and` WebsiteCoreTests` (the library unit tests).
|
||||
|
||||
## Deployment
|
||||
The production image is built for `linux/amd64` in release mode with a statically linked Swift runtime and jemalloc, runs as a non-root `hummingbird` user, and exposes port `8080`(`ENTRYPOINT ./Website --http-host 0.0.0.0 --http-port 8080`).
|
||||
Build, tag, and push a release to the registry (an explicit version is required):
|
||||
```sh
|
||||
make img-release version=1.2.3
|
||||
```
|
||||
|
||||
Pull and run the prebuilt image in production — the `-f docker-compose.yml` flag is important, as it skips the local-development override:
|
||||
```sh
|
||||
docker compose -f docker-compose.yml pull
|
||||
docker compose -f docker-compose.yml up -d
|
||||
```
|
||||
|
||||
### Required variables
|
||||
The Makefile and Compose files read these from a `.env` file (or the environment). Provide your own values — do **not** commit secrets.
|
||||
| Variable | Used for |
|
||||
| --- | --- |
|
||||
| `HOST_CONTAINER` | Container registry host (e.g. `registry.example.com`). |
|
||||
| `HOST_OWNER` | Registry namespace / owner. |
|
||||
| `HOST_USER`, `HOST_PASSWORD` | Registry credentials for `make img-release`. |
|
||||
| `IMAGE_NAME`, `IMAGE_TAG` | Image name and tag. |
|
||||
| `IMAGE_PLATFORM` | Build platform (e.g. `linux/amd64`). |
|
||||
| `HOST_PORT` | Host port mapped to the container's `8080` (default `8080`). |
|
||||
| `LOG_LEVEL` | Runtime log level (default `info`). |
|
||||
| `HTTP_SERVER_NAME` | Runtime server name (default `LoudWebsite`). |
|
||||
| `SECURITY_STRICT_TRANSPORT_SECURITY` | HSTS header value (default `max-age=31536000; includeSubDomains`). |
|
||||
|
||||
Reference in New Issue
Block a user