mirror of
https://github.com/storytold/photocraft.git
synced 2026-10-13 14:59:06 +00: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 <noreply@anthropic.com>
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/).
|
||||
|
||||
To self-host the browser version with Docker, see [Docker web hosting](packaging/web/README.md#docker).
|
||||
|
||||
## 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).
|
||||
|
||||
@@ -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.
|
||||
|
||||
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:
|
||||
|
||||
- **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.
|
||||
|
||||
## 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
|
||||
|
||||
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