Tweaks and fixes throughout the project (#27)

This PR contains the work done to do a little bit of housekeeping pass across all packages and the Website service.

To provide further details about the work:
* Refreshed the READMEs and source documentation to match the current code;
* Tagged every test case consistently across the Infrastructure, Localization, Persistence, and Website test targets;
* Removed Website middleware tests now covered by Infrastructure's own suite;
* Conformed the `PrepareDB` method to Sendable;
* Relaxes the production Compose DATABASE_TLS default from require to prefer;
* Added Persistence test verifying the prefer posture falls back to plaintext connections.

Reviewed-on: rock-n-code/loud-amsterdam#27
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:
2026-07-30 06:33:57 +00:00
committed by javier
parent 0887135328
commit ea00b94841
71 changed files with 633 additions and 512 deletions
+11 -14
View File
@@ -8,14 +8,13 @@ ARG OXIPNG_VERSION=9.1.5
ARG SVGO_VERSION=4.0.2
# Install the minifiers in their own layer, so they are cached across asset changes.
# The oxipng pin is fuzzy (=~) so Alpine package revision bumps (-r0, -r1, ...) do
# not break the build when the base image advances.
# The oxipng pin is fuzzy (=~) so Alpine package revision bumps (-r0, -r1, ...) do not break the build when the base
# image advances.
RUN apk add --no-cache "oxipng=~${OXIPNG_VERSION}" \
&& npm install --global "esbuild@${ESBUILD_VERSION}" "svgo@${SVGO_VERSION}"
# Copy the static files and minify the JS/CSS/SVG sources and losslessly recompress
# the PNG images in place, keeping their names so the URL paths derived from the
# StaticFile enumeration stay unchanged.
# Copy the static files and minify the JS/CSS/SVG sources and losslessly recompress the PNG images in place, keeping
# their names so the URL paths derived from the StaticFile enumeration stay unchanged.
WORKDIR /static
COPY ./Services/Website/Resources/Static .
RUN esbuild --minify --allow-overwrite --outdir=css css/*.css \
@@ -23,8 +22,8 @@ RUN esbuild --minify --allow-overwrite --outdir=css css/*.css \
&& oxipng --opt max --strip safe *.png \
&& svgo --recursive --folder .
# Export stage: `docker build --target assets-export --output <dir>` writes the
# minified static files to <dir> for local inspection.
# Export stage: `docker build --target assets-export --output <dir>` writes the minified static files to <dir> for
# local inspection.
FROM scratch AS assets-export
COPY --from=assets /static /
@@ -44,17 +43,16 @@ RUN export DEBIAN_FRONTEND=noninteractive DEBCONF_NONINTERACTIVE_SEEN=true \
WORKDIR /build
# First just resolve dependencies.
# This creates a cached layer that can be reused as long as the manifests do
# not change. The Website package depends on the local Localization package via
# a relative path, so its manifest must be present for resolution to succeed.
# This creates a cached layer that can be reused as long as the manifests do not change. The Website package depends on
# the local Localization package via a relative path, so its manifest must be present for resolution to succeed.
COPY ./Packages/Localization/Package.swift ./Packages/Localization/
COPY ./Packages/Persistence/Package.swift ./Packages/Persistence/
COPY ./Packages/Infrastructure/Package.swift ./Packages/Infrastructure/
COPY ./Services/Website/Package.swift ./Services/Website/Package.resolved ./Services/Website/
RUN swift package --package-path ./Services/Website resolve
# Copy only the Swift inputs needed for a release build. Static assets are built
# in the assets stage and copied into staging after the binary is produced.
# Copy only the Swift inputs needed for a release build. Static assets are built in the assets stage and copied into
# staging after the binary is produced.
COPY ./Packages/Infrastructure/Sources ./Packages/Infrastructure/Sources
COPY ./Packages/Localization/Sources ./Packages/Localization/Sources
COPY ./Packages/Persistence/Sources ./Packages/Persistence/Sources
@@ -79,8 +77,7 @@ RUN cp "/usr/libexec/swift/linux/swift-backtrace-static" ./
# Copy resources bundled by SPM to staging area
RUN find -L "$(swift build --package-path /build/Services/Website -c release --show-bin-path)/" -regex '.*\.resources$' -exec cp -Ra {} ./ \;
# Create the static files directory (served by FileMiddleware) and fill it with
# the minified copies from the assets stage
# Create the static files directory (served by FileMiddleware) and fill it with the minified copies from the assets stage
RUN mkdir -p ./Resources/Static
COPY --from=assets /static ./Resources/Static
+12 -10
View File
@@ -34,13 +34,14 @@ The persistence backend runs as a `Fluent` service inside the application's Serv
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)
LocalizationMiddleware (negotiates the request's language)
NotFoundMiddleware (renders the localized 404 page on .notFound)
FileMiddleware (serves Resources/Static)
RootController (GET / → landing page)
HealthController (GET /health → liveness, GET /health/ready → readiness)
→ SecurityHeadersMiddleware (security headers on every response)
VaryMiddleware (marks every response as varying on Accept-Encoding)
ResponseCompressionMiddleware (gzip/deflate above the size threshold)
LocalizationMiddleware (negotiates the request's language)
NotFoundMiddleware (renders the localized 404 page on .notFound)
→ FileMiddleware (serves Resources/Static)
RootController (GET / → landing page)
HealthController (GET /health → liveness, GET /health/ready → readiness)
```
## Configuration
@@ -108,6 +109,7 @@ See [Persistence](#persistence-1) below for the workflow.
| `path.staticFiles` | `PATH_STATIC_FILES` | `Resources/Static` | Directory, relative to the working directory, that static files are served from. |
### Rate limiting
These keys configure the `RateLimitMiddleware` budget for the upcoming newsletter subscription endpoint. They are read at startup, but the middleware is **not yet attached to any route** — the values have no effect until the subscription endpoint ships.
| Config key | Environment variable | Default | Description |
| --- | --- | --- | --- |
| `rateLimit.limit` | `RATELIMIT_LIMIT` | `5` | Requests admitted per client per window on the subscribe endpoint; the excess is answered with `429 Too Many Requests` and a `Retry-After` header. |
@@ -182,7 +184,7 @@ 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 the service's two targets — `WebsiteTests` (the executable/integration tests) and `WebsiteLibraryTests` (the library unit tests) — plus the local packages' suites: `WebTests`, `PersistenceTests`, and `LocalizationTests`.
Tests use the [Swift Testing](https://developer.apple.com/documentation/testing/) framework. The `Tests/Website.xctestplan` covers the service's two targets — `WebsiteTests` (the executable/integration tests) and `WebsiteLibraryTests` (the library unit tests) — plus the local packages' suites: `InfrastructureTests`, `PersistenceTests`, and `LocalizationTests`.
The `Persistence` package has its own suite (run it from `Packages/Persistence`). Its tests run against the in-memory backend by default; the MySQL integration test is skipped unless a database is pointed at via `MYSQL_TEST_HOST` (with optional `MYSQL_TEST_PORT`/`NAME`/`USERNAME`/`PASSWORD`), so `swift test` stays runnable with no database:
```sh
@@ -259,8 +261,8 @@ The Makefile and Compose files read these from a `.env` file (or the environment
| `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`). |
| `DATABASE_DRIVER` | `inMemory` (default) or `mysql`. Set to `mysql` in production to use a managed database. |
| `DATABASE_DRIVER` | `inMemory` or `mysql`. The production Compose file defaults it to `mysql`; the local override defaults back to the in-memory backend. |
| `DATABASE_HOST`, `DATABASE_PORT`, `DATABASE_NAME`, `DATABASE_USERNAME`, `DATABASE_PASSWORD` | MySQL/MariaDB connection (when `DATABASE_DRIVER=mysql`). Provide the password via a secret. |
| `DATABASE_TLS` | TLS posture when connecting: `off`, `prefer`, or `require` (default `require` in production). |
| `DATABASE_TLS` | TLS posture when connecting: `off`, `prefer`, or `require` (default `prefer` in production — set `require` when the database enforces TLS, so a stripped connection fails instead of silently downgrading to plaintext). |
Run the migrations against the production database once before (or during) rollout: `docker compose -f docker-compose.yml run --rm website --database-migrate`.
+3 -3
View File
@@ -31,9 +31,9 @@ struct App {
]
)
// Migrate-and-exit mode runs the registered migrations against the configured backend and returns,
// so a shared database is migrated by a single deliberate invocation (`--database-migrate`) rather
// than by every booting instance.
// Migrate-and-exit mode runs the registered migrations against the configured backend and returns, so a shared
// database is migrated by a single deliberate invocation (`--database-migrate`) rather than by every booting
// instance.
guard !reader.migrate else {
try await migration(
reader: reader
@@ -25,8 +25,8 @@ func application(
logLevel: reader.logLevel
)
// A broken catalog degrades to serving raw localization keys rather than failing, so it is
// only ever visible to visitors surface it here instead.
// A broken catalog degrades to serving raw localization keys rather than failing, so it is only ever visible to
// visitors surface it here instead.
if languages.catalogState != .loaded {
let isCatalogMissing = languages.catalogState == .missing
@@ -63,8 +63,8 @@ func application(
app.addServices(fluent)
// The in-memory backend is recreated on every launch, so it is migrated on startup. The MySQL/MariaDB
// backend is left untouched here: a shared database is migrated out of band to avoid multi-instance races.
// The in-memory backend is recreated on every launch, so it is migrated on startup. The MySQL/MariaDB backend is
// left untouched here: a shared database is migrated out of band to avoid multi-instance races.
if case .inMemory = reader.driver {
app.beforeServerStarts {
try await fluent.migrate()
@@ -137,8 +137,7 @@ private func logger(
/// larger than `minimumResponseSizeToCompress` when the client advertises support, the localization middleware that negotiates the request's
/// language from its `Accept-Language` header, the not-found middleware that serves the error page, and the static file middleware that serves the
/// contents of `staticFilesPath` (tagging responses with the given `cacheControl` directives), then adds the `RootController` routes that
/// render the landing page, the `SubscriptionController` routes that register newsletter subscriptions, and the `HealthController` routes
/// that serve the health check.
/// render the landing page, and the `HealthController` routes that serve the health check.
///
/// The security-headers middleware sits just inside request logging so it covers every response that reaches a client the landing page, the compressed
/// responses, the rendered error page, and the served static files.
@@ -162,8 +161,8 @@ private func router(
logLevel: Logger.Level,
probe: Probe
) -> Router<AppRequestContext> {
// HEAD siblings are generated for every GET route, so uptime monitors and crawlers probing
// with HEAD requests get the page's status and headers instead of a 404.
// HEAD siblings are generated for every GET route, so uptime monitors and crawlers probing with HEAD requests get
// the page's status and headers instead of a 404.
let router = Router(
context: AppRequestContext.self,
options: .autoGenerateHeadEndpoints
@@ -123,9 +123,9 @@ package extension ConfigReader {
/// The rate limit applied to the subscription endpoint, built from the `rateLimit.*` keys.
///
/// `rateLimit.limit` requests are admitted per client per `rateLimit.window` seconds. When
/// `rateLimit.trustForwardedFor` is set, clients are keyed by the first `X-Forwarded-For` entry
/// enable it only behind a reverse proxy that sets the header, since clients can forge it otherwise.
/// `rateLimit.limit` requests are admitted per client per `rateLimit.window` seconds. When `rateLimit.trustForwardedFor` is set,
/// clients are keyed by the first `X-Forwarded-For` entry enable it only behind a reverse proxy that sets the header, since clients can forge it
/// otherwise.
var rateLimit: RateLimitMiddleware<AppRequestContext>.Configuration {
.init(
limit: int(
@@ -2,9 +2,8 @@ import Infrastructure
/// A static file shipped with the website service.
///
/// Each case identifies a file name stored under the static files root (the `Resources/Static`
/// directory) and served by Hummingbird's `FileMiddleware` middleware. A name can be available
/// with more than one extension (see ``fileExtensions``), each resolving to its own file.
/// Each case identifies a file name stored under the static files root (the `Resources/Static` directory) and served by Hummingbird's
/// `FileMiddleware` middleware. A name can be available with more than one extension (see ``fileExtensions``), each resolving to its own file.
enum StaticFile: Asset, CaseIterable {
/// The `apple-touch-icon.png` icon.
case appleTouchIcon
@@ -22,8 +22,7 @@ struct IndexPage {
/// Creates a landing page localized to the given locale.
/// - Parameters:
/// - locale: the locale the page content is localized to.
/// - assetVersion: the version token appended to the page's asset URLs, or `nil` (the
/// default) to leave them unversioned.
/// - assetVersion: the version token appended to the page's asset URLs, or `nil` (the default) to leave them unversioned.
init(
locale: Locale,
assetVersion: String? = nil
@@ -4,9 +4,8 @@ import NIOCore
/// The website's request context.
///
/// Extends the core request storage with the negotiated language, defaulting to the default
/// supported language until ``LocalizationMiddleware`` resolves it from the request, and with
/// the connected client's address, so ``RateLimitMiddleware`` can key its budgets per client.
/// Extends the core request storage with the negotiated language, defaulting to the default supported language until ``LocalizationMiddleware``
/// resolves it from the request, and with the connected client's address, so ``RateLimitMiddleware`` can key its budgets per client.
public struct WebsiteRequestContext: LocalizedRequestContext, RemoteAddressRequestContext {
// MARK: Properties
@@ -28,8 +28,7 @@ public struct HealthController<Context: RequestContext> {
// MARK: Initializers
/// Creates a health controller.
/// - Parameter probe: the probe consulted by the readiness route; when `nil`, only the liveness
/// route is served.
/// - Parameter probe: the probe consulted by the readiness route; when `nil`, only the liveness route is served.
public init(
probe: Probe? = nil
) {
@@ -72,9 +71,8 @@ private extension HealthController {
/// Handles a request for the liveness check.
///
/// Returns a constant JSON body built directly per request the payload is a tiny literal with no
/// rendering step, so there is nothing to pre-render or cache. It reports only that the process is up,
/// with no dependency check, so an orchestrator restarts the process only when the process itself is
/// Returns a constant JSON body built directly per request the payload is a tiny literal with no rendering step, so there is nothing to pre-render or
/// cache. It reports only that the process is up, with no dependency check, so an orchestrator restarts the process only when the process itself is
/// unresponsive.
/// - Parameters:
/// - request: the incoming request.
@@ -93,9 +91,8 @@ private extension HealthController {
/// Handles a request for the readiness check.
///
/// Consults the `Probe` supplied at initialization and reports `200 OK` when the service's
/// database is reachable, or `503 Service Unavailable` otherwise, so a load balancer withholds
/// traffic from an instance that cannot yet serve it without restarting the process.
/// Consults the `Probe` supplied at initialization and reports `200 OK` when the service's database is reachable, or `503 Service Unavailable`
/// otherwise, so a load balancer withholds traffic from an instance that cannot yet serve it without restarting the process.
/// - Parameters:
/// - request: the incoming request.
/// - context: the context the request is resolved against.
@@ -4,8 +4,7 @@ import Infrastructure
/// Serves the website's root routes.
///
/// The controller exposes its routes through its `RouterController` conformance, so the
/// application that composes it registers them declaratively:
/// The controller exposes its routes through its `RouterController` conformance, so the application that composes it registers them declaratively:
///
/// ```swift
/// router.addController {
@@ -13,8 +12,7 @@ import Infrastructure
/// }
/// ```
///
/// - Note: `Context` is the request context the routes are resolved against, and must match the
/// context of the router the routes are added to.
/// - Note: `Context` is the request context the routes are resolved against, and must match the context of the router the routes are added to.
public struct RootController<Context: LocalizedRequestContext> {
// MARK: Properties
@@ -25,8 +23,7 @@ public struct RootController<Context: LocalizedRequestContext> {
// MARK: Initializers
/// Creates a root controller.
/// - Parameter assetVersion: the version token appended to the page's asset URLs, or `nil`
/// (the default) to leave them unversioned.
/// - Parameter assetVersion: the version token appended to the page's asset URLs, or `nil` (the default) to leave them unversioned.
public init(
assetVersion: String? = nil
) {
@@ -67,8 +64,7 @@ private extension RootController {
/// Handles a request for the landing page.
///
/// Renders the ``IndexPage`` in the language stored on the context by ``LocalizationMiddleware``,
/// falling back to the default language.
/// Renders the ``IndexPage`` in the language stored on the context by ``LocalizationMiddleware``, falling back to the default language.
/// - Parameters:
/// - request: the incoming request.
/// - context: the context the request is resolved against.
@@ -3,7 +3,10 @@ import Testing
@testable import WebsiteLibrary
@Suite("StaticFile enumeration")
@Suite(
"StaticFile enumeration",
.tags(.enumeration)
)
struct StaticFileTests {
// MARK: Type aliases
@@ -4,7 +4,10 @@ import Testing
@testable import WebsiteLibrary
@Suite("ErrorPage page", .tags(.page))
@Suite(
"ErrorPage page",
.tags(.page)
)
struct ErrorPageTests {
// MARK: Functional tests
@@ -4,7 +4,10 @@ import Testing
@testable import WebsiteLibrary
@Suite("IndexPage page", .tags(.page))
@Suite(
"IndexPage page",
.tags(.page)
)
struct IndexPageTests {
// MARK: Functional tests
@@ -7,7 +7,10 @@ import Testing
@testable import WebsiteLibrary
@Suite("HealthController controller", .tags(.controller))
@Suite(
"HealthController controller",
.tags(.controller)
)
struct HealthControllerTests {
// MARK: Functional tests
@@ -6,7 +6,10 @@ import Testing
@testable import WebsiteLibrary
@Suite("RootController controller", .tags(.controller))
@Suite(
"RootController controller",
.tags(.controller)
)
struct RootControllerTests {
// MARK: Constants
@@ -1,57 +0,0 @@
import HTTPTypes
import Hummingbird
import HummingbirdTesting
import NIOCore
import Testing
import Infrastructure
@testable import WebsiteLibrary
@Suite("LocalizationMiddleware middleware", .tags(.middleware))
struct LocalizationMiddlewareTests {
// MARK: Constants
private let app: Application = .init(router: {
let router = Router(context: WebsiteRequestContext.self)
router.addMiddleware {
LocalizationMiddleware()
}
router.get("language") { _, context in
context.language
}
return router
}())
// MARK: Functional tests
@Test
func `negotiates a supported language from the header`() async throws {
try await app.test(.router) { client in
try await client.execute(
uri: "/language",
method: .get,
headers: [.acceptLanguage: "en-US,en;q=0.9"]
) { response in
#expect(String(buffer: response.body) == "en")
}
}
}
@Test
func `falls back to the default without a header`() async throws {
try await app.test(.router) { client in
try await client.execute(
uri: "/language",
method: .get
) { response in
#expect(String(buffer: response.body) == "en")
}
}
}
}
@@ -1,173 +0,0 @@
import Hummingbird
import HummingbirdTesting
import NIOCore
import Testing
import Infrastructure
@testable import WebsiteLibrary
@Suite("NotFoundMiddleware middleware", .tags(.middleware))
struct NotFoundMiddlewareTests {
// MARK: Constants
private let app: Application = .init(router: {
let router = Router(context: WebsiteRequestContext.self)
router.addMiddleware {
LocalizationMiddleware()
NotFoundMiddleware()
}
router.get("hello") { _, _ in
"Hello!"
}
router.get("boom") { _, _ -> String in
throw HTTPError(.badRequest)
}
return router
}())
// MARK: Functional tests
@Test
func `renders the error page for an unmatched request`() async throws {
try await app.test(.router) { client in
try await client.execute(
uri: "/this-path-does-not-exist",
method: .get
) { response in
let body = String(buffer: response.body)
#expect(response.status == .notFound)
#expect(response.headers[.contentType] == "text/html; charset=utf-8")
#expect(response.headers[.contentLanguage] == "en")
#expect(response.headers[.vary] == "Accept-Language")
#expect(body.contains("Page Not Found"))
}
}
}
@Test
func `passes a matched response through untouched`() async throws {
try await app.test(.router) { client in
try await client.execute(
uri: "/hello",
method: .get
) { response in
#expect(response.status == .ok)
#expect(response.body == ByteBuffer(string: "Hello!"))
}
}
}
@Test
func `rethrows a non-not-found error unchanged`() async throws {
try await app.test(.router) { client in
try await client.execute(
uri: "/boom",
method: .get
) { response in
let body = String(buffer: response.body)
#expect(response.status == .badRequest)
#expect(!body.contains("Page Not Found"))
}
}
}
@Test
func `renders versioned asset URLs when given a version`() async throws {
try await app(
assetVersion: "0123456789abcdef"
).test(.router) { client in
try await client.execute(
uri: "/this-path-does-not-exist",
method: .get
) { response in
let body = String(buffer: response.body)
#expect(body.contains("/css/error.css?v=0123456789abcdef"))
#expect(body.contains("/js/shared.js?v=0123456789abcdef"))
}
}
}
@Test
func `renders unversioned asset URLs by default`() async throws {
try await app.test(.router) { client in
try await client.execute(
uri: "/this-path-does-not-exist",
method: .get
) { response in
let body = String(buffer: response.body)
#expect(body.contains(#"href="/css/error.css""#))
#expect(!body.contains("?v="))
}
}
}
@Test
func `serves the error page without revalidation headers`() async throws {
// A `304 Not Modified` only ever stands in for a success, so the error page must not
// invite revalidation with an entity tag or a cache policy.
try await app.test(.router) { client in
try await client.execute(
uri: "/this-path-does-not-exist",
method: .get
) { response in
#expect(response.status == .notFound)
#expect(response.headers[.eTag] == nil)
#expect(response.headers[.cacheControl] == nil)
}
}
}
@Test
func `serves the full error page to a conditional request`() async throws {
try await app.test(.router) { client in
try await client.execute(
uri: "/this-path-does-not-exist",
method: .get,
headers: [.ifNoneMatch: "*"]
) { response in
let body = String(buffer: response.body)
#expect(response.status == .notFound)
#expect(body.contains("Page Not Found"))
}
}
}
}
// MARK: - Helpers
private extension NotFoundMiddlewareTests {
// MARK: Methods
/// Builds an application whose not-found middleware appends the given version token to the
/// error page's asset URLs.
/// - Parameter assetVersion: the version token appended to the page's asset URLs.
/// - Returns: the configured application.
func app(
assetVersion: String?
) -> some ApplicationProtocol {
let router = Router(context: WebsiteRequestContext.self)
router.addMiddleware {
LocalizationMiddleware()
NotFoundMiddleware(
assetVersion: assetVersion
)
}
return Application(router: router)
}
}
@@ -3,8 +3,8 @@ import Testing
extension Tag {
/// Tests exercising a controller of the Website library.
@Tag static var controller: Tag
/// Tests exercising a middleware of the Website library.
@Tag static var middleware: Tag
/// Tests exercising an enumeration of the Website library.
@Tag static var enumeration: Tag
/// Tests exercising a page of the Website library.
@Tag static var page: Tag
}
+6 -6
View File
@@ -1,12 +1,12 @@
# Local development overrides.
# Compose merges this file on top of docker-compose.yml automatically, so a
# plain `docker compose up` builds from source instead of pulling a registry image:
# Compose merges this file on top of docker-compose.yml automatically, so a plain `docker compose up` builds from
# source instead of pulling a registry image:
#
# docker compose up --build # build locally and run
# docker compose up -d # reuse the last local build
#
# It reuses the `image:` name from the base file, so the local build is tagged
# the same way the production image would be.
# It reuses the `image:` name from the base file, so the local build is tagged the same way the production image would
# be.
services:
website:
image: ${IMAGE_NAME}:${IMAGE_TAG:-latest}
@@ -20,8 +20,8 @@ services:
DATABASE_HOST: ${DATABASE_HOST:-localhost}
DATABASE_TLS: ${DATABASE_TLS:-off}
# Local development database, started only with the `database` profile so a plain
# `docker compose up` still runs the in-memory backend:
# Local development database, started only with the `database` profile so a plain `docker compose up` still runs the
# in-memory backend:
#
# docker compose --profile database up mariadb
mariadb:
+5 -7
View File
@@ -6,9 +6,8 @@ name: loud-platform
# docker compose -f docker-compose.yml pull
# docker compose -f docker-compose.yml up -d
#
# The `-f docker-compose.yml` flag is important in production: it skips the
# docker-compose.override.yml file, which Compose would otherwise merge in
# automatically for local development.
# The `-f docker-compose.yml` flag is important in production: it skips the docker-compose.override.yml file, which
# Compose would otherwise merge in automatically for local development.
services:
website:
image: ${HOST_CONTAINER}/${HOST_OWNER}/${IMAGE_NAME}:${IMAGE_TAG:-latest}
@@ -21,16 +20,15 @@ services:
LOG_LEVEL: ${LOG_LEVEL:-info}
HTTP_SERVER_NAME: ${HTTP_SERVER_NAME:-LoudWebsite}
SECURITY_STRICT_TRANSPORT_SECURITY: "${SECURITY_STRICT_TRANSPORT_SECURITY:-max-age=31536000; includeSubDomains}"
# Persistence: in-memory by default; set DATABASE_DRIVER=mysql to run against a
# managed MySQL/MariaDB database. Provide the password via the environment or a
# secret — never commit it.
# Persistence: in-memory by default; set DATABASE_DRIVER=mysql to run against a managed MySQL/MariaDB database.
# Provide the password via the environment or a secret — never commit it.
DATABASE_DRIVER: ${DATABASE_DRIVER:-mysql}
DATABASE_HOST: ${DATABASE_HOST:-localhost}
DATABASE_PORT: ${DATABASE_PORT:-3306}
DATABASE_NAME: ${DATABASE_NAME:-loud-ams}
DATABASE_USERNAME: ${DATABASE_USERNAME:-loud-ams}
DATABASE_PASSWORD: ${DATABASE_PASSWORD:-}
DATABASE_TLS: ${DATABASE_TLS:-require}
DATABASE_TLS: ${DATABASE_TLS:-prefer}
healthcheck:
test: ["CMD", "curl", "--fail", "--silent", "--show-error", "http://127.0.0.1:8080/health"]
interval: 30s