Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
67 changes: 67 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
# Anything that ends up in a Docker build context but is not needed to
# produce a runnable image. Keeping this list accurate dramatically
# speeds up `docker build` and avoids leaking local state into images.

# VCS metadata
.git
.gitignore
.gitattributes

# Docker artefacts (we don't want the previous build context inside a
# build context, that way lies recursion)
Dockerfile
default.conf
docker-compose.yml
.dockerignore
Comment on lines +12 to +15

# Dependencies — installed fresh inside the build stage
node_modules
**/node_modules

# Build output — produced inside the build stage, never copy from host
dist
doc-dist
.vuepress/dist
.umi
.umi-production
lib-cov
coverage
.nyc_output

# Logs, caches, editor & OS junk
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
.cache
.parcel-cache
.eslintcache
.DS_Store
Thumbs.db
.idea
.vscode
.history
tmp
temp

# Tests
__tests__
*.test.js
*.test.ts
*.test.tsx
*.spec.js
*.spec.ts
*.spec.tsx
coverage
.nyc_output

# CI / release files that have no business inside a runtime image
.github
.husky
CODEOWNERS
*.md
!readme.md
!zh.md
Comment on lines +63 to +65
!CHANGELOG.md
!DOCKER.md
98 changes: 98 additions & 0 deletions DOCKER.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
# Running h5-Dooring with Docker / docker compose

This document explains how to build and run the project end-to-end
without installing Node.js, yarn or any other build dependency on
your host machine. It is a drop-in solution for self-hosting and
closes the long-standing "I just want a working image" gap that
several users have asked about (see upstream issue #162).

The setup is a two-stage Docker build:

1. A `builder` stage compiles the umi-based React SPA into static
files in `dist/`.
2. A `runtime` stage copies those files into an `nginx:alpine`
image that serves them on port 80.

A hardened `default.conf` is also provided so that HTML5 history
routing (`/editor`, `/preview`, `/ide`, …) Just Works without
returning nginx 404s when the user refreshes the page.

## Quick start (docker compose, recommended)

```sh
# from the repository root
docker compose up --build
```

Once the build finishes, open
[http://localhost:8080/](http://localhost:8080/) in a browser. To
stop and remove the container, press `Ctrl+C` or run:

```sh
docker compose down
```

If port `8080` is already in use, edit the `ports:` mapping at the
top of `docker-compose.yml` and change `"8080:80"` to, say,
`"9000:80"`.

## Quick start (plain `docker`)

```sh
docker build -t h5-dooring:latest .
docker run --rm -p 8080:80 --name h5-dooring h5-dooring:latest
```

Stop it with `docker stop h5-dooring` and remove the container with
`docker rm h5-dooring`.

## Behind a reverse proxy (Nginx Proxy Manager, Traefik, Caddy, …)

The container listens on port 80 inside its own network namespace,
which is exactly what reverse proxies expect. Point your proxy at
`http://h5-dooring:80` (docker compose network) or
`http://127.0.0.1:8080` (host) and you are done. No additional
headers need to be set — the SPA does not rely on any proxy
metadata.

## Why is the OpenSSL legacy provider set in the Dockerfile?

The `start` and `build` scripts in `package.json` set
`NODE_OPTIONS=--openssl-legacy-provider`. This is a well-known
workaround for webpack 4 on Node 17+, where the default OpenSSL
provider can no longer load the legacy hashes the bundler still
uses. We bake the same flag into the build stage so the in-image
build behaves identically to a local `yarn start`.

## Customising the build

* **Different Node version** — edit the `FROM node:16-bullseye AS
builder` line in `Dockerfile`. Node 14 and 16 are both known to
work; later versions may need the OpenSSL workaround more often.
* **Different nginx base** — replace `nginx:1.25-alpine` with any
other nginx image that understands `conf.d/*.conf`. The bundled
`default.conf` is plain nginx syntax.
* **Custom nginx config** — edit `default.conf`; it is copied into
`/etc/nginx/conf.d/default.conf` at image build time.

## Troubleshooting

* **`/editor` returns 404** — make sure you did not delete
`default.conf`. SPA history routing depends on the
`try_files $uri $uri/ /index.html;` directive.
* **Out of disk space** — the `dist/` folder is created inside the
build stage and is not kept on the host, so a failed `docker
compose down --rmi all` will not leak large files. Use `docker
system prune` periodically.
* **Port already in use** — change the host-side port in
`docker-compose.yml` as described above.

## Files added by this change

| File | Purpose |
| --------------------- | ------------------------------------------------ |
| `Dockerfile` | Multi-stage build (deps → build → nginx). |
| `default.conf` | nginx config with SPA history fallback + gzip. |
| `.dockerignore` | Trims the build context for speed and safety. |
| `docker-compose.yml` | One-command self-hosting setup. |
| `DOCKER.md` | This document. |
63 changes: 61 additions & 2 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -1,2 +1,61 @@
FROM nginx:latest
COPY ./default.conf /etc/nginx/conf.d/default.conf
# syntax=docker/dockerfile:1.6
#
# Multi-stage Dockerfile for h5-Dooring.
#
# Build stage compiles the umi-based SPA with the legacy OpenSSL provider
# required by webpack 4 (see the `start`/`build` scripts in package.json).
# The runtime stage copies the static `dist/` output into a tiny nginx
# image so the result is portable and easy to self-host.
#
# Usage:
# docker build -t h5-dooring:latest .
# docker run --rm -p 8080:80 h5-dooring:latest
#
# Then open http://localhost:8080/ in a browser.

# ---------- Build stage ----------
FROM node:16-bullseye AS builder

ENV NODE_OPTIONS=--openssl-legacy-provider \
CI=true \
NPM_CONFIG_LOGLEVEL=warn \
YARN_VERSION=1.22.19

# Install yarn pinned to the version most likely to match the existing
# yarn.lock (this project ships a yarn.lock; falling back to npm would
# produce a different dependency graph and break reproducible builds).
RUN corepack enable && corepack prepare yarn@${YARN_VERSION} --activate

WORKDIR /app

# Copy manifests first so the dependency layer is cached when only sources
# change. This is the standard Docker "cache friendly" ordering.
COPY package.json yarn.lock ./

# Install all dependencies (including devDependencies required by umi to
# compile the source). --frozen-lockfile guarantees we honour yarn.lock
# exactly, matching what every other contributor's local install does.
RUN yarn install --frozen-lockfile --network-timeout 600000

# Now copy the rest of the source tree and produce a production build.
COPY . .

# Build the static SPA. The umi build outputs to ./dist (see .umirc.ts).
RUN yarn build

# ---------- Runtime stage ----------
FROM nginx:1.25-alpine AS runtime

# Replace the default nginx site with one that knows how to serve an
# umi SPA (history-mode fallback to index.html, gzip, cache headers).
COPY default.conf /etc/nginx/conf.d/default.conf

# Copy the built static assets from the builder stage.
COPY --from=builder /app/dist /usr/share/nginx/html

# Expose the default HTTP port. Override at run-time with `-p`.
EXPOSE 80

# nginx:alpine already defines a correct CMD, but be explicit so that
# users reading the Dockerfile see exactly what runs.
CMD ["nginx", "-g", "daemon off;"]
61 changes: 61 additions & 0 deletions default.conf
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
# nginx configuration for the h5-Dooring SPA.
#
# h5-Dooring is a client-side React/umi application that uses HTML5
# history routing (see .umirc.ts -> routes). That means every
# "deep link" the user might hit (e.g. /editor, /preview, /ide)
# must be rewritten to /index.html so the SPA can take over and the
# router can render the right page on the client.

server {
listen 80;
listen [::]:80;
server_name _;

# Use the umi build output directory as the document root.
root /usr/share/nginx/html;
index index.html;

# Cap upload size generously: form-designer uploads and the
# "download source" feature can produce multi-MB JSON payloads.
client_max_body_size 25m;

# gzip text responses; the SPA ships a lot of JSON/JS/CSS.
gzip on;
gzip_vary on;
gzip_min_length 1024;
gzip_proxied any;
gzip_comp_level 6;
gzip_types
text/plain
text/css
text/javascript
application/javascript
application/json
application/xml
image/svg+xml;

# Long-lived caching for fingerprinted assets; short for the
# entry HTML so deploys are picked up immediately.
location ~* \.(?:js|css|woff2?|ttf|otf|eot|svg|png|jpg|jpeg|gif|webp|ico)$ {
expires 7d;
access_log off;
add_header Cache-Control "public, max-age=604800, immutable";
try_files $uri =404;
}

# The single-page-application fallback. Anything that is not a
# real file on disk is served as index.html so the client router
# can handle it.
location / {
try_files $uri $uri/ /index.html;
add_header Cache-Control "no-cache";
}

# Friendly error pages that still go through the SPA so deep links
# such as /editor recover to the editor instead of an nginx error.
error_page 404 /index.html;
error_page 500 502 503 504 /index.html;
Comment on lines +56 to +57

# Hide nginx version in `Server` headers — small hardening win.
server_tokens off;
}
55 changes: 55 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
# docker-compose definition for self-hosting h5-Dooring locally.
#
# This file addresses the long-standing feature request to make the
# project easy to self-host without manually installing Node, yarn,
# webpack and friends. See upstream issue #162:
# https://github.com/MrXujiang/h5-Dooring/issues/162
#
# Usage:
# docker compose up --build
# # or detached:
# docker compose up -d --build
#
# Then open http://localhost:8080/ in a browser.
#
# The image is multi-stage: the `builder` service produces the static
# SPA, and the `web` service serves it with a hardened nginx config
# that is also aware of HTML5 history routing (see default.conf).

name: h5-dooring

services:
web:
build:
context: .
dockerfile: Dockerfile
image: h5-dooring:latest
container_name: h5-dooring
restart: unless-stopped
ports:
# Map host:container. Change the left-hand side if 8080 is taken.
- "8080:80"
Comment on lines +27 to +31
# Read-only root filesystem for a small security uplift; the only
# place nginx needs to write is /var/cache/nginx and /var/run, both
# of which are already tmpfs in the official nginx:alpine image.
read_only: true
tmpfs:
- /var/cache/nginx:size=32m
- /var/run:size=4m
healthcheck:
# curl lives in the nginx:alpine image and we use the official
# entrypoint behaviour: hit / which is served as the SPA shell.
test: ["CMD", "wget", "-qO-", "http://127.0.0.1/"]
Comment on lines +40 to +42
interval: 30s
timeout: 5s
retries: 3
start_period: 10s
# Drop all Linux capabilities and run as the unprivileged nginx
# user that ships with the base image. This is the recommended
# hardening for any container that only needs to listen on a port.
security_opt:
- no-new-privileges:true
cap_drop:
- ALL
cap_add:
- NET_BIND_SERVICE