Hardened the Docker build of the Website service target.
This commit is contained in:
@@ -10,6 +10,9 @@
|
||||
**/.build
|
||||
**/.swiftpm
|
||||
|
||||
# Test sources
|
||||
**/Tests
|
||||
|
||||
# Xcode project (not used by the Linux build)
|
||||
*.xcodeproj
|
||||
|
||||
|
||||
@@ -20,8 +20,11 @@ HOST_USER=
|
||||
# Name of the Docker image to pull/run.
|
||||
IMAGE_NAME=website
|
||||
|
||||
# Platform of the Docker image to pull/run.
|
||||
IMAGE_PLATFORM=linux/arm64
|
||||
# Platform of the deployment image
|
||||
IMAGE_PLATFORM=linux/amd64
|
||||
|
||||
# Platform of the *local* build only (docker-compose.override.yml)
|
||||
BUILD_PLATFORM=linux/arm64
|
||||
|
||||
# Tag of the image to pull/run.
|
||||
# Use a semver in production; avoid `latest` so rollbacks are unambiguous.
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# ================================
|
||||
# Asset image
|
||||
# ================================
|
||||
FROM node:22-alpine AS assets
|
||||
FROM node:22-alpine@sha256:c610fcdfb1d5b4740dd70c284ed3cb16bb857e0f7166196e36a5501df7a3aa32 AS assets
|
||||
|
||||
ARG ESBUILD_VERSION=0.28.1
|
||||
ARG OXIPNG_VERSION=9.1.5
|
||||
@@ -30,7 +30,7 @@ COPY --from=assets /static /
|
||||
# ================================
|
||||
# Build image
|
||||
# ================================
|
||||
FROM swift:6.3-noble AS build
|
||||
FROM swift:6.3-noble@sha256:69bf1f0281e13d82c9e49d67c2dd1dcc8c00bad738c860f4323d7078787ec8ea AS build
|
||||
|
||||
# Install OS updates
|
||||
RUN export DEBIAN_FRONTEND=noninteractive DEBCONF_NONINTERACTIVE_SEEN=true \
|
||||
@@ -59,10 +59,15 @@ COPY ./Packages/Localization/Sources ./Packages/Localization/Sources
|
||||
COPY ./Packages/Persistence/Sources ./Packages/Persistence/Sources
|
||||
COPY ./Packages/Utility/Sources ./Packages/Utility/Sources
|
||||
COPY ./Services/Website/Sources ./Services/Website/Sources
|
||||
COPY ./Services/Website/Tests ./Services/Website/Tests
|
||||
|
||||
# Build the application, with optimizations, with static linking, and using jemalloc
|
||||
RUN swift build --package-path ./Services/Website -c release \
|
||||
# Build the application, with optimizations, with static linking, and using jemalloc.
|
||||
#
|
||||
# SPM validates the path of every target in the root package — including the test targets — even when only the
|
||||
# executable product is built, so those directories have to exist. They are created empty instead of copied: the test
|
||||
# sources are not needed to compile the product, keeping them out means editing a test never invalidates this layer,
|
||||
# and it keeps the local database bind mount under Tests/DB out of the image entirely.
|
||||
RUN mkdir -p ./Services/Website/Tests/App ./Services/Website/Tests/Library \
|
||||
&& swift build --package-path ./Services/Website -c release \
|
||||
--product "Website" \
|
||||
--static-swift-stdlib \
|
||||
-Xlinker -ljemalloc
|
||||
@@ -89,7 +94,7 @@ RUN chmod -R a-w ./Resources
|
||||
# ================================
|
||||
# Run image
|
||||
# ================================
|
||||
FROM ubuntu:noble
|
||||
FROM ubuntu:noble@sha256:561618e2c15bf2397621dd04f96926663a3b5616c189cf7e38db7e82f5c538ea
|
||||
|
||||
# Make sure all system packages are up to date, and install only essential packages.
|
||||
RUN export DEBIAN_FRONTEND=noninteractive DEBCONF_NONINTERACTIVE_SEEN=true \
|
||||
|
||||
@@ -126,7 +126,7 @@ ast-minify: ## Preview the minified JS/CSS assets in .build/minified
|
||||
.PHONY: img-check
|
||||
img-check: ## Check the production image builds
|
||||
@docker build \
|
||||
--platform linux/amd64 \
|
||||
--platform $(IMAGE_PLATFORM) \
|
||||
--file Dockerfile \
|
||||
../..
|
||||
|
||||
|
||||
@@ -246,7 +246,20 @@ cd ../../Packages/Persistence && POSTGRES_TEST_HOST=127.0.0.1 swift test # again
|
||||
`POSTGRES_TEST_NAME`/`USERNAME`/`PASSWORD` each default to `site` (and `POSTGRES_TEST_PORT` is optional), so pass the password explicitly when the local container was initialised with a different `DATABASE_PASSWORD`.
|
||||
|
||||
## Deployment
|
||||
The production image is built in release mode with a statically linked Swift runtime and jemalloc, runs as a non-root `hummingbird` user, and exposes port `8080`. `make img-check` pins the build to `linux/amd64`; `make img-release` builds for `IMAGE_PLATFORM`.
|
||||
The production image is built in release mode with a statically linked Swift runtime and jemalloc, runs as a non-root `hummingbird` user, and exposes port `8080`.
|
||||
|
||||
`IMAGE_PLATFORM` is the single source of truth for the deployment architecture: `make img-check` and `make img-release` both build for it, and `docker-compose.yml` runs the pulled image with it. Keep it matched to the deployment host — the three have to agree, or a release builds for one architecture and the production Compose file refuses to run it. Local development builds are separate and follow `BUILD_PLATFORM` (see `docker-compose.override.yml`), since they target your machine rather than the deployment.
|
||||
|
||||
The test sources are deliberately absent from the image. SPM validates the path of every target in the root package — including the test targets — even when only the executable product is built, so the Dockerfile creates those two directories empty rather than copying them. That keeps test edits from invalidating the release-build layer, and keeps the local database bind mount under `Tests/DB` out of the build context entirely (`.dockerignore` excludes `**/Tests` for the same reason).
|
||||
|
||||
### Base images
|
||||
All three stages pin their base by digest as well as tag, so a rebuild of an old commit resolves the same bases it originally used. The trade-off is that they no longer pick up upstream rebuilds on their own: **refresh the digests deliberately**, on whatever cadence you patch on, with
|
||||
|
||||
```sh
|
||||
docker buildx imagetools inspect swift:6.3-noble --format '{{.Manifest.Digest}}'
|
||||
```
|
||||
|
||||
Both build stages are discarded — only `ubuntu:noble` ships — and the build and run stages each `apt-get dist-upgrade`, so OS packages are patched at build time regardless of the pin's age. What a stale pin holds back is the base layer itself.
|
||||
|
||||
The executable is the `ENTRYPOINT` and its serving flags are the `CMD`:
|
||||
```dockerfile
|
||||
@@ -256,7 +269,7 @@ CMD ["--http-host", "0.0.0.0", "--http-port", "8080"]
|
||||
The split is what makes the migrate-and-exit invocation below work: `docker compose run --rm website --database-migrate` replaces the `CMD` flags without having to override the entrypoint.
|
||||
|
||||
```sh
|
||||
make img-check # verify it builds for linux/amd64, without tagging or publishing
|
||||
make img-check # verify it builds for IMAGE_PLATFORM, without tagging or publishing
|
||||
make img-release version=1.2.3 # build, tag, and push a release (an explicit version is required)
|
||||
```
|
||||
|
||||
@@ -269,7 +282,7 @@ docker compose -f docker-compose.yml up -d
|
||||
### Static assets
|
||||
`Resources/Static` holds the site's stylesheets and scripts under `css/` and `js/`, paired by name: `shared.*` is loaded by every page, alongside a per-page `index.*` and `not-found.*`. The remaining files — the icons, `site.webmanifest`, `robots.txt`, and `sitemap.xml` — sit at the root. Every one of them is a case of the `StaticFile` enumeration, which is what the pages derive their URLs from.
|
||||
|
||||
The image build optimizes the files under `Resources/Static` in its `assets` stage, in place, with pinned optimizer versions so asset output is reproducible for a given Dockerfile commit:
|
||||
The image build optimizes the files under `Resources/Static` in its `assets` stage, in place, with pinned optimizer versions — and on a base image pinned by digest, not just by tag — so asset output is reproducible for a given Dockerfile commit:
|
||||
- CSS and JS are minified with [esbuild](https://esbuild.github.io) (every file in `css/` and `js/`).
|
||||
- PNG images are losslessly recompressed with [oxipng](https://github.com/oxipng/oxipng), recursively — the output is pixel-identical, only encoded smaller.
|
||||
- SVGs are minified with [svgo](https://github.com/svg/svgo), recursively.
|
||||
@@ -321,7 +334,8 @@ The Makefile and Compose files read these from `.env` (or the environment). Prov
|
||||
| `HOST_OWNER` | Registry namespace / owner. |
|
||||
| `HOST_USER`, `HOST_PASSWORD` | Registry credentials for `make img-release`. |
|
||||
| `IMAGE_NAME`, `IMAGE_TAG` | Image name and tag. `IMAGE_TAG` is the fallback for `make img-release`'s `version=` argument. |
|
||||
| `IMAGE_PLATFORM` | Build platform for `make img-release` (e.g. `linux/amd64`). `make img-check` pins `linux/amd64` regardless. |
|
||||
| `IMAGE_PLATFORM` | Deployment architecture (default `linux/amd64`). Drives `make img-check`, `make img-release`, and the `platform:` the production Compose file runs the pulled image with — keep it matched to the deployment host. |
|
||||
| `BUILD_PLATFORM` | Architecture of the *local* development build only (default `linux/arm64`); set it to `linux/amd64` on an Intel Mac or an amd64 Linux box. Never used by the production Compose file. |
|
||||
| `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 `SiteWebsite`). |
|
||||
|
||||
@@ -10,7 +10,9 @@
|
||||
services:
|
||||
website:
|
||||
image: ${IMAGE_NAME}:${IMAGE_TAG:-latest}
|
||||
platform: linux/arm64
|
||||
# The local build targets the development machine, not the deployment: this cancels the base file's
|
||||
# ${IMAGE_PLATFORM}. Set BUILD_PLATFORM=linux/amd64 in .env when developing on an Intel Mac or an amd64 Linux box.
|
||||
platform: ${BUILD_PLATFORM:-linux/arm64}
|
||||
build:
|
||||
context: ../..
|
||||
dockerfile: Services/Website/Dockerfile
|
||||
|
||||
@@ -11,7 +11,7 @@ name: site-platform
|
||||
services:
|
||||
website:
|
||||
image: ${HOST_CONTAINER}/${HOST_OWNER}/${IMAGE_NAME}:${IMAGE_TAG:-latest}
|
||||
platform: linux/amd64
|
||||
platform: ${IMAGE_PLATFORM:-linux/amd64}
|
||||
container_name: ${HOST_OWNER}-${IMAGE_NAME}
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
|
||||
Reference in New Issue
Block a user