mirror of
https://github.com/storytold/photocraft.git
synced 2026-10-14 11:16:53 -04:00
Self-host the web app with Docker (nginx, community-maintained) (#1334)
A multi-stage Dockerfile builds photocraft-web with Trunk and serves dist/web from an unprivileged nginx on :8080, with /healthz, precompressed gzip, the wasm MIME type and immutable hashed assets. Rust and Trunk follow the release workflow, and the PHOTOCRAFT_BUILD_SHA/DATE build args stamp About. Documented in packaging/web/README.md; not built in CI. Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
1 parent
9c554e2ead
commit
f0ae12dd22
6 files changed
+215
No files matched your search
@@ -0,0 +1,18 @@
|
|||||||
|
.git
|
||||||
|
.github
|
||||||
|
.agents
|
||||||
|
.codex
|
||||||
|
.aws
|
||||||
|
**/target
|
||||||
|
dist
|
||||||
|
corpus
|
||||||
|
log
|
||||||
|
plan
|
||||||
|
secrets
|
||||||
|
temp
|
||||||
|
craft-fonts
|
||||||
|
book/output
|
||||||
|
**/.DS_Store
|
||||||
|
**/.env
|
||||||
|
**/.env.*
|
||||||
|
*.log
|
||||||
+68
@@ -0,0 +1,68 @@
|
|||||||
|
# syntax=docker/dockerfile:1
|
||||||
|
# Community-maintained self-hosting recipe (see packaging/web/README.md#docker); the official
|
||||||
|
# web build is the `web` job in .github/workflows/release.yml (packaging/web/package.sh).
|
||||||
|
#
|
||||||
|
# Source of truth for the toolchain: release.yml and ci.yml build with the latest *stable* Rust
|
||||||
|
# (dtolnay/rust-toolchain@stable), not a pinned version. This default is the stable release CI
|
||||||
|
# used when it was last bumped; raise it when CI's stable moves on. It must stay >= the
|
||||||
|
# workspace `rust-version` in Cargo.toml.
|
||||||
|
ARG RUST_VERSION=1.99
|
||||||
|
ARG NGINX_VERSION=1.28-alpine
|
||||||
|
|
||||||
|
FROM rust:${RUST_VERSION}-slim-bookworm AS builder
|
||||||
|
# Must match `TRUNK_VERSION` in the `web` job of .github/workflows/release.yml.
|
||||||
|
ARG TRUNK_VERSION=0.21.14
|
||||||
|
RUN apt-get update \
|
||||||
|
&& apt-get install -y --no-install-recommends ca-certificates curl \
|
||||||
|
&& rm -rf /var/lib/apt/lists/* \
|
||||||
|
&& rustup target add wasm32-unknown-unknown
|
||||||
|
RUN set -eu; \
|
||||||
|
case "$(uname -m)" in \
|
||||||
|
x86_64) arch=x86_64 ;; \
|
||||||
|
aarch64) arch=aarch64 ;; \
|
||||||
|
*) echo 'The web builder supports amd64 and arm64' >&2; exit 1 ;; \
|
||||||
|
esac; \
|
||||||
|
asset="trunk-${arch}-unknown-linux-musl.tar.gz"; \
|
||||||
|
url="https://github.com/trunk-rs/trunk/releases/download/v${TRUNK_VERSION}"; \
|
||||||
|
cd /tmp; \
|
||||||
|
curl -fsSL --retry 3 -A Photocraft-dev "$url/$asset" -o "$asset"; \
|
||||||
|
curl -fsSL --retry 3 -A Photocraft-dev "$url/$asset.sha256" -o "$asset.sha256"; \
|
||||||
|
printf '%s %s\n' "$(cat "$asset.sha256")" "$asset" | sha256sum -c -; \
|
||||||
|
tar -xzf "$asset" -C /usr/local/bin trunk; \
|
||||||
|
rm "$asset" "$asset.sha256"; \
|
||||||
|
trunk --version
|
||||||
|
|
||||||
|
WORKDIR /src
|
||||||
|
COPY . .
|
||||||
|
WORKDIR /src/apps/photocraft-web
|
||||||
|
# Docker serving has no static-host per-file size cap. Thin LTO uses less build
|
||||||
|
# memory than the release zip's fat LTO; retain the profile's other optimizations.
|
||||||
|
ARG CARGO_PROFILE_WASM_RELEASE_LTO=thin
|
||||||
|
ARG CARGO_BUILD_JOBS=1
|
||||||
|
# Optional build provenance shown in About and `--version` (crates/engine/src/build_info.rs).
|
||||||
|
# .git is not in the build context, so pass it in, e.g.
|
||||||
|
# --build-arg PHOTOCRAFT_BUILD_SHA=$(git rev-parse HEAD) --build-arg PHOTOCRAFT_BUILD_DATE=$(date -u +%F)
|
||||||
|
# Left empty, the build reports itself as a dev build.
|
||||||
|
ARG PHOTOCRAFT_BUILD_SHA=
|
||||||
|
ARG PHOTOCRAFT_BUILD_DATE=
|
||||||
|
# index.html selects the wasm-release profile, HEIF support and wasm-opt -Oz.
|
||||||
|
RUN --mount=type=cache,target=/usr/local/cargo/registry \
|
||||||
|
--mount=type=cache,target=/src/target \
|
||||||
|
--mount=type=cache,target=/root/.cache/trunk \
|
||||||
|
trunk build --release --locked \
|
||||||
|
&& test -s /src/dist/web/index.html \
|
||||||
|
&& gzip -9 -k /src/dist/web/*.wasm /src/dist/web/*.js /src/dist/web/index.html
|
||||||
|
|
||||||
|
FROM nginx:${NGINX_VERSION} AS runtime
|
||||||
|
COPY packaging/web/nginx.conf /etc/nginx/nginx.conf
|
||||||
|
COPY --from=builder /src/dist/web/ /usr/share/nginx/html/
|
||||||
|
COPY LICENSE-MIT LICENSE-APACHE NOTICE ATTRIBUTION.md /usr/share/nginx/html/
|
||||||
|
COPY assets/fonts/OFL-*.txt assets/icons/LICENSE-*.txt assets/dict/LICENSE-*.txt \
|
||||||
|
crates/ui-egui/src/i18n/LICENSE-translations.txt /usr/share/nginx/html/licenses/
|
||||||
|
# Bypass the stock entrypoint: this configuration needs no runtime rewriting.
|
||||||
|
USER nginx
|
||||||
|
EXPOSE 8080
|
||||||
|
ENTRYPOINT []
|
||||||
|
CMD ["nginx", "-g", "daemon off;"]
|
||||||
|
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
|
||||||
|
CMD wget -q -O /dev/null http://127.0.0.1:8080/healthz || exit 1
|
||||||
@@ -304,6 +304,8 @@ Maintainers: [`docs/releasing.md`](docs/releasing.md) explains how releases are
|
|||||||
|
|
||||||
Developer, architecture, automation, format, and security documentation is maintained in the [PhotoCraft documentation book](book/).
|
Developer, architecture, automation, format, and security documentation is maintained in the [PhotoCraft documentation book](book/).
|
||||||
|
|
||||||
|
To self-host the browser version with Docker, see [Docker web hosting](packaging/web/README.md#docker).
|
||||||
|
|
||||||
## Security
|
## Security
|
||||||
|
|
||||||
Security architecture, threat modeling, parser hardening, fuzzing, and vulnerability reporting are covered in the [security documentation](book/src/security/) and the repository [security policy](SECURITY.md).
|
Security architecture, threat modeling, parser hardening, fuzzing, and vulnerability reporting are covered in the [security documentation](book/src/security/) and the repository [security policy](SECURITY.md).
|
||||||
|
|||||||
@@ -303,6 +303,16 @@ Any static file server works for `dist/web`, for example `python3 -m http.server
|
|||||||
|
|
||||||
URL flags: `?webgl` forces the WebGL2 backend, and `?cpu` forces the CPU canvas path.
|
URL flags: `?webgl` forces the WebGL2 backend, and `?cpu` forces the CPU canvas path.
|
||||||
|
|
||||||
|
To build and serve the web app entirely in Docker, run from the repository root:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
docker build --load -t photocraft-web:local .
|
||||||
|
docker run --rm -p 8080:8080 photocraft-web:local
|
||||||
|
```
|
||||||
|
|
||||||
|
Open http://localhost:8080/. See [Docker hosting](../packaging/web/README.md#docker) for
|
||||||
|
HTTPS/reverse-proxy deployment and browser limitations.
|
||||||
|
|
||||||
How the web shell (`apps/photocraft-web/src/web.rs`) differs from desktop:
|
How the web shell (`apps/photocraft-web/src/web.rs`) differs from desktop:
|
||||||
|
|
||||||
- **Open** uses `rfd::AsyncFileDialog`. The bytes arrive asynchronously in `Services::inbox`, which the app drains every frame.
|
- **Open** uses `rfd::AsyncFileDialog`. The bytes arrive asynchronously in `Services::inbox`, which the app drains every frame.
|
||||||
|
|||||||
@@ -12,6 +12,65 @@ static site in `photocraft-web-<version>/`:
|
|||||||
|
|
||||||
There is no server-side code. Upload the folder's contents anywhere that serves static files.
|
There is no server-side code. Upload the folder's contents anywhere that serves static files.
|
||||||
|
|
||||||
|
## Docker
|
||||||
|
|
||||||
|
> The Docker recipe (`Dockerfile`, `.dockerignore`, `packaging/web/nginx.conf`) is
|
||||||
|
> **community-maintained**: it is not built in CI or used for releases, so it can lag behind the
|
||||||
|
> official web build above. Fixes are welcome.
|
||||||
|
|
||||||
|
From the repository root (Docker is the only build prerequisite):
|
||||||
|
|
||||||
|
```sh
|
||||||
|
docker build --load -t photocraft-web:local .
|
||||||
|
docker run -d --name photocraft-web --restart unless-stopped \
|
||||||
|
-p 8080:8080 photocraft-web:local
|
||||||
|
```
|
||||||
|
|
||||||
|
Open **http://localhost:8080/**. The multi-stage `Dockerfile` builds the existing Rust/Wasm
|
||||||
|
app with Trunk, including HEIF support, then copies only the static site into NGINX. The
|
||||||
|
runtime runs as the `nginx` user on port **8080**, with a `/healthz` endpoint and Docker
|
||||||
|
health check. It serves precompressed gzip assets, the Wasm MIME type, immutable hashed
|
||||||
|
assets, and an HTML page that revalidates after deployments. No Rust installation, Node.js,
|
||||||
|
database, GPU passthrough, or document volume is needed on the host.
|
||||||
|
|
||||||
|
The first build downloads Rust dependencies and Trunk's Wasm tools and can take several
|
||||||
|
minutes. Subsequent builds reuse Cargo caches. The builder supports Linux amd64 and arm64;
|
||||||
|
the default Rust version is the stable release CI used when the `Dockerfile` was last updated
|
||||||
|
(CI and releases always use the latest stable), and Trunk is pinned to the release workflow's
|
||||||
|
`TRUNK_VERSION`. `RUST_VERSION`, `TRUNK_VERSION`, and `NGINX_VERSION` can be overridden with
|
||||||
|
`--build-arg`. To stamp the About dialog with the commit and date instead of "dev build", pass
|
||||||
|
`--build-arg PHOTOCRAFT_BUILD_SHA=$(git rev-parse HEAD) --build-arg PHOTOCRAFT_BUILD_DATE=$(date -u +%F)`.
|
||||||
|
The web build does not embed the optional `craft-fonts` checkout.
|
||||||
|
|
||||||
|
The Docker build uses thin LTO and one Cargo build job to reduce peak memory, while keeping
|
||||||
|
the web profile's size optimizations and `wasm-opt -Oz`. Its Wasm size may differ from the
|
||||||
|
release zip figures below; NGINX has no 25 MiB file limit. To use the release zip's fat LTO on
|
||||||
|
a builder with more memory, pass `--build-arg CARGO_PROFILE_WASM_RELEASE_LTO=fat`.
|
||||||
|
`--build-arg CARGO_BUILD_JOBS=4` enables more concurrent compilation on larger builders. A compiler killed with
|
||||||
|
`SIGKILL`/`cannot allocate memory` means the Docker/Podman VM needs more memory or swap.
|
||||||
|
|
||||||
|
For a public deployment, put an HTTPS reverse proxy or your hosting platform's TLS endpoint
|
||||||
|
in front of container port 8080. With a reverse proxy on the same host, bind only loopback
|
||||||
|
(`-p 127.0.0.1:8080:8080`); with a proxy container, connect both through a Docker network.
|
||||||
|
HTTPS enables WebGPU and browser clipboard APIs. If hosting under `/photocraft/`, redirect
|
||||||
|
`/photocraft` to `/photocraft/` and strip that prefix when proxying to the container. Relative
|
||||||
|
asset URLs then work without rebuilding. For example, in an existing HTTPS NGINX server:
|
||||||
|
|
||||||
|
```nginx
|
||||||
|
location = /photocraft { return 301 /photocraft/; }
|
||||||
|
location /photocraft/ {
|
||||||
|
proxy_pass http://127.0.0.1:8080/;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
The editor and image processing run on the visitor's device. Open uses the browser file
|
||||||
|
picker; Save and Export download files. Preferences use browser localStorage. There is no
|
||||||
|
server-side document storage, desktop TCP control server, or web autosave/crash recovery;
|
||||||
|
save/download work before closing or reloading the page. A browser with WebGPU or WebGL2 is
|
||||||
|
required; `?webgl` forces the fallback when troubleshooting.
|
||||||
|
|
||||||
|
To verify the browser deployment, open the page, open an image and save/export it.
|
||||||
|
|
||||||
## Sizes
|
## Sizes
|
||||||
|
|
||||||
Measured on the 0.2.x build (`packaging/web/package.sh`; gzip `-9`, Brotli quality 11):
|
Measured on the 0.2.x build (`packaging/web/package.sh`; gzip `-9`, Brotli quality 11):
|
||||||
|
|||||||
@@ -0,0 +1,58 @@
|
|||||||
|
worker_processes auto;
|
||||||
|
pid /tmp/nginx.pid;
|
||||||
|
error_log /dev/stderr warn;
|
||||||
|
|
||||||
|
events {
|
||||||
|
worker_connections 1024;
|
||||||
|
}
|
||||||
|
|
||||||
|
http {
|
||||||
|
include /etc/nginx/mime.types;
|
||||||
|
default_type application/octet-stream;
|
||||||
|
access_log /dev/stdout;
|
||||||
|
sendfile on;
|
||||||
|
server_tokens off;
|
||||||
|
|
||||||
|
# All runtime writes go to /tmp, allowing a read-only container filesystem.
|
||||||
|
client_body_temp_path /tmp/client_temp;
|
||||||
|
proxy_temp_path /tmp/proxy_temp;
|
||||||
|
fastcgi_temp_path /tmp/fastcgi_temp;
|
||||||
|
uwsgi_temp_path /tmp/uwsgi_temp;
|
||||||
|
scgi_temp_path /tmp/scgi_temp;
|
||||||
|
|
||||||
|
# Precompressed at build time; uncompressed clients receive the original.
|
||||||
|
gzip_static on;
|
||||||
|
gzip_vary on;
|
||||||
|
|
||||||
|
server {
|
||||||
|
listen 8080;
|
||||||
|
listen [::]:8080;
|
||||||
|
root /usr/share/nginx/html;
|
||||||
|
index index.html;
|
||||||
|
add_header X-Content-Type-Options nosniff always;
|
||||||
|
|
||||||
|
location = /healthz {
|
||||||
|
access_log off;
|
||||||
|
default_type text/plain;
|
||||||
|
return 200 "ok\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
location = /index.html {
|
||||||
|
add_header Cache-Control no-cache always;
|
||||||
|
add_header X-Content-Type-Options nosniff always;
|
||||||
|
}
|
||||||
|
|
||||||
|
# Trunk gives these files content hashes. Missing assets must be 404s,
|
||||||
|
# never an HTML fallback or an error cached for a year.
|
||||||
|
location ~ \.(wasm|js)$ {
|
||||||
|
types { application/wasm wasm; text/javascript js; }
|
||||||
|
add_header Cache-Control "public, max-age=31536000, immutable";
|
||||||
|
add_header X-Content-Type-Options nosniff always;
|
||||||
|
try_files $uri =404;
|
||||||
|
}
|
||||||
|
|
||||||
|
location / {
|
||||||
|
try_files $uri $uri/ =404;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in new issue
Block a user