mirror of
https://github.com/fabro-sh/fabro.git
synced 2026-08-28 05:27:41 +00:00
feat(docker): publish multi-arch server image to GHCR
Replace the docker/Dockerfile-based api+web compose setup with a single root Dockerfile that runs the fabro server with the embedded web UI on port 80, persists state under /storage, and drops to a non-root fabro user with CAP_NET_BIND_SERVICE. The release workflow stages the prebuilt Linux binaries from the compile job into a buildx context and publishes multi-arch images to ghcr.io/fabro-sh/fabro as :<version> (always), :latest (stable tags only), and :nightly (nightly tags only). Also address zizmor findings in nightly.yml (pinned create-github-app-token, persist-credentials: false with explicit remote URL setup) and release.yml (no-cache on tag-triggered setup-bun to close the cache-poisoning path). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
e891cb3961
commit
d52e6829c2
12 changed files with 173 additions and 152 deletions
4
.dockerignore
Normal file
4
.dockerignore
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
*
|
||||
!docker/entrypoint.sh
|
||||
!docker/settings.toml
|
||||
!docker-context/**
|
||||
8
.github/workflows/nightly.yml
vendored
8
.github/workflows/nightly.yml
vendored
|
|
@ -22,7 +22,7 @@ jobs:
|
|||
steps:
|
||||
- name: Mint GitHub App token
|
||||
id: app-token
|
||||
uses: actions/create-github-app-token@v2
|
||||
uses: actions/create-github-app-token@fee1f7d63c2ff003460e3d139729b119787bc349 # v2.2.2
|
||||
with:
|
||||
app-id: ${{ vars.FABRO_RELEASES_APP_ID }}
|
||||
private-key: ${{ secrets.FABRO_RELEASES_APP_PRIVATE_KEY }}
|
||||
|
|
@ -30,7 +30,7 @@ jobs:
|
|||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
fetch-depth: 0 # full history + tags for nightly probing
|
||||
token: ${{ steps.app-token.outputs.token }}
|
||||
persist-credentials: false
|
||||
|
||||
- name: Skip if HEAD is already a nightly tag
|
||||
id: skip
|
||||
|
|
@ -43,6 +43,8 @@ jobs:
|
|||
|
||||
- uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0
|
||||
if: steps.skip.outputs.skip != 'true'
|
||||
with:
|
||||
no-cache: true
|
||||
|
||||
- name: Install bun deps (for SPA verify)
|
||||
if: steps.skip.outputs.skip != 'true'
|
||||
|
|
@ -65,4 +67,6 @@ jobs:
|
|||
run: |
|
||||
git config user.name "fabro-releases[bot]"
|
||||
git config user.email "fabro-releases[bot]@users.noreply.github.com"
|
||||
git remote set-url origin \
|
||||
"https://x-access-token:${GH_TOKEN}@github.com/${GITHUB_REPOSITORY}.git"
|
||||
bin/dev/release.sh nightly
|
||||
|
|
|
|||
60
.github/workflows/release.yml
vendored
60
.github/workflows/release.yml
vendored
|
|
@ -22,6 +22,8 @@ jobs:
|
|||
with:
|
||||
persist-credentials: false
|
||||
- uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0
|
||||
with:
|
||||
no-cache: true
|
||||
- run: bun install
|
||||
- run: scripts/refresh-fabro-spa.sh
|
||||
- run: git diff --exit-code -- lib/crates/fabro-spa/assets
|
||||
|
|
@ -101,6 +103,64 @@ jobs:
|
|||
fi
|
||||
gh release create "$TAG_NAME" target/distrib/* --generate-notes $PRERELEASE_FLAG
|
||||
|
||||
docker:
|
||||
name: Docker image
|
||||
needs: compile
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
||||
with:
|
||||
path: target/distrib
|
||||
pattern: artifacts-fabro-*-linux-gnu
|
||||
merge-multiple: true
|
||||
|
||||
- name: Stage binaries for Docker build context
|
||||
run: |
|
||||
for target in x86_64-unknown-linux-gnu aarch64-unknown-linux-gnu; do
|
||||
case "$target" in
|
||||
x86_64-*) arch=amd64 ;;
|
||||
aarch64-*) arch=arm64 ;;
|
||||
esac
|
||||
mkdir -p "docker-context/$arch"
|
||||
tar -xzf "target/distrib/fabro-${target}.tar.gz" -C target/distrib
|
||||
cp "target/distrib/fabro-${target}/fabro" "docker-context/$arch/fabro"
|
||||
done
|
||||
|
||||
- uses: docker/setup-qemu-action@29109295f81e9208d7d86ff1c6c12d2833863392 # v3.6.0
|
||||
|
||||
- uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 # v3.10.0
|
||||
|
||||
- uses: docker/login-action@9780b0c442fbb1117ed29e0efdff1e18412f7567 # v3.3.0
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- id: meta
|
||||
uses: docker/metadata-action@902fa8ec7d6ecbf8d84d538b9b233a880e428804 # v5.7.0
|
||||
with:
|
||||
images: ghcr.io/fabro-sh/fabro
|
||||
tags: |
|
||||
type=match,pattern=v(.*),group=1
|
||||
type=raw,value=latest,enable=${{ !contains(github.ref_name, '-') }}
|
||||
type=raw,value=nightly,enable=${{ contains(github.ref_name, '-nightly.') }}
|
||||
|
||||
- uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6.18.0
|
||||
with:
|
||||
context: .
|
||||
file: Dockerfile
|
||||
platforms: linux/amd64,linux/arm64
|
||||
push: true
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
|
||||
update-homebrew:
|
||||
name: Update Homebrew Formula
|
||||
needs: release
|
||||
|
|
|
|||
44
Dockerfile
Normal file
44
Dockerfile
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
# syntax=docker/dockerfile:1.9
|
||||
#
|
||||
# Runtime image for the Fabro server.
|
||||
#
|
||||
# Binaries are supplied pre-built via the release workflow:
|
||||
# docker-context/amd64/fabro (x86_64-unknown-linux-gnu)
|
||||
# docker-context/arm64/fabro (aarch64-unknown-linux-gnu)
|
||||
#
|
||||
# The image serves the HTTP API (with embedded web UI) on port 80,
|
||||
# persists state to /storage, and runs as the unprivileged `fabro` user.
|
||||
|
||||
FROM debian:trixie-slim
|
||||
|
||||
ARG TARGETARCH
|
||||
|
||||
ENV DEBIAN_FRONTEND=noninteractive
|
||||
RUN apt-get update \
|
||||
&& apt-get install -y --no-install-recommends \
|
||||
ca-certificates \
|
||||
git \
|
||||
libcap2-bin \
|
||||
tini \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
RUN groupadd --system --gid 1000 fabro \
|
||||
&& useradd --system --uid 1000 --gid fabro \
|
||||
--home-dir /var/fabro --shell /usr/sbin/nologin fabro \
|
||||
&& install -d -o fabro -g fabro -m 0755 /var/fabro /storage \
|
||||
&& install -d -m 0755 /etc/fabro
|
||||
|
||||
COPY --chmod=0755 docker-context/${TARGETARCH}/fabro /usr/local/bin/fabro
|
||||
RUN setcap cap_net_bind_service=+ep /usr/local/bin/fabro
|
||||
|
||||
COPY docker/settings.toml /etc/fabro/settings.toml
|
||||
COPY --chmod=0755 docker/entrypoint.sh /usr/local/bin/fabro-entrypoint
|
||||
|
||||
ENV FABRO_HOME=/var/fabro \
|
||||
FABRO_CONFIG=/etc/fabro/settings.toml
|
||||
|
||||
VOLUME ["/storage"]
|
||||
EXPOSE 80
|
||||
|
||||
ENTRYPOINT ["/usr/bin/tini", "--", "/usr/local/bin/fabro-entrypoint"]
|
||||
CMD ["fabro", "server", "start", "--foreground", "--bind", "0.0.0.0:80"]
|
||||
27
bin/dev/docker-build.sh
Executable file
27
bin/dev/docker-build.sh
Executable file
|
|
@ -0,0 +1,27 @@
|
|||
#!/usr/bin/env bash
|
||||
# Build a local fabro Docker image from the current working tree.
|
||||
#
|
||||
# The release workflow uses prebuilt release tarballs, but for local development
|
||||
# we cargo build first and stage the binary where the Dockerfile expects it.
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
cd "$(dirname "$0")/../.."
|
||||
|
||||
case "$(uname -m)" in
|
||||
x86_64|amd64) arch=amd64 ;;
|
||||
aarch64|arm64) arch=arm64 ;;
|
||||
*)
|
||||
echo "unsupported host arch: $(uname -m)" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
echo "Building fabro (release, host arch: $arch)..."
|
||||
cargo build --release -p fabro-cli
|
||||
|
||||
mkdir -p "docker-context/$arch"
|
||||
cp target/release/fabro "docker-context/$arch/fabro"
|
||||
|
||||
echo "Building Docker image..."
|
||||
docker build -t fabro .
|
||||
|
|
@ -1,30 +0,0 @@
|
|||
FROM rust:1-bookworm@sha256:4ec71e955e6c08aeb238885083222ddff79d82eb87654a96c76e38e94da1a53b AS rust-builder
|
||||
WORKDIR /app
|
||||
COPY Cargo.toml Cargo.lock ./
|
||||
COPY lib/crates lib/crates
|
||||
COPY openapi openapi
|
||||
RUN cargo build --release --bin fabro
|
||||
|
||||
FROM oven/bun:1@sha256:c9aa897b6028d54e510c4babd41bcdd481ad4a4b5030ad31135a9cabfe2c24df
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
python3 make g++ ca-certificates nodejs npm && \
|
||||
rm -rf /var/lib/apt/lists/*
|
||||
RUN npm i -g mintlify@4.2.507 && \
|
||||
sed -i 's/const version = __VERSION__/const version = "0.0.0"/' \
|
||||
/usr/local/lib/node_modules/mintlify/node_modules/katex/dist/katex.mjs
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY --from=rust-builder /app/target/release/fabro /usr/local/bin/fabro
|
||||
|
||||
COPY package.json bun.lock ./
|
||||
COPY apps/fabro-web/package.json apps/fabro-web/
|
||||
COPY lib/packages/fabro-api-client/package.json lib/packages/fabro-api-client/
|
||||
RUN bun install --frozen-lockfile
|
||||
|
||||
COPY apps/fabro-web apps/fabro-web
|
||||
COPY lib/packages/fabro-api-client lib/packages/fabro-api-client
|
||||
COPY docs docs
|
||||
COPY docker/entrypoint.ts .
|
||||
|
||||
ENTRYPOINT ["bun", "run", "entrypoint.ts"]
|
||||
|
|
@ -1,19 +0,0 @@
|
|||
[api]
|
||||
base_url = "http://api:3000"
|
||||
|
||||
[features]
|
||||
session_sandboxes = false
|
||||
|
||||
[checkpoint]
|
||||
exclude_globs = [
|
||||
"**/node_modules/**",
|
||||
"**/.pnpm-store/**",
|
||||
"**/.npm/**",
|
||||
"**/.cache/**",
|
||||
"**/playwright-report/**",
|
||||
"**/test-results/**",
|
||||
"**/.cargo-target*/**",
|
||||
"**/.cargo_target*/**",
|
||||
"**/.wasm-pack/**",
|
||||
"**/.tmpbuild/**",
|
||||
]
|
||||
|
|
@ -1,35 +1,13 @@
|
|||
services:
|
||||
api:
|
||||
fabro:
|
||||
image: fabro
|
||||
build:
|
||||
context: ..
|
||||
dockerfile: docker/Dockerfile
|
||||
platforms:
|
||||
- linux/arm64
|
||||
pull_policy: never
|
||||
command: ["api"]
|
||||
dockerfile: Dockerfile
|
||||
ports:
|
||||
- "3000:3000"
|
||||
|
||||
web:
|
||||
image: fabro
|
||||
pull_policy: never
|
||||
command: ["web"]
|
||||
ports:
|
||||
- "5173:5173"
|
||||
environment:
|
||||
- FABRO_DEMO=1
|
||||
- "80:80"
|
||||
volumes:
|
||||
- ./demo-settings.toml:/root/.fabro/settings.toml:ro
|
||||
- ../apps/fabro-web/public:/app/apps/fabro-web/public
|
||||
depends_on:
|
||||
- api
|
||||
- fabro-storage:/storage
|
||||
|
||||
docs:
|
||||
image: fabro
|
||||
pull_policy: never
|
||||
command: ["docs"]
|
||||
ports:
|
||||
- "3333:3333"
|
||||
volumes:
|
||||
- ../docs:/app/docs:ro
|
||||
volumes:
|
||||
fabro-storage:
|
||||
|
|
|
|||
12
docker/entrypoint.sh
Executable file
12
docker/entrypoint.sh
Executable file
|
|
@ -0,0 +1,12 @@
|
|||
#!/bin/sh
|
||||
set -eu
|
||||
|
||||
# When started as root (the default), ensure the storage volume is writable
|
||||
# by the unprivileged fabro user, then drop privileges. File capabilities on
|
||||
# /usr/local/bin/fabro grant CAP_NET_BIND_SERVICE so it can still bind port 80.
|
||||
if [ "$(id -u)" = 0 ]; then
|
||||
chown fabro:fabro /storage
|
||||
exec runuser -u fabro -- "$@"
|
||||
fi
|
||||
|
||||
exec "$@"
|
||||
|
|
@ -1,44 +0,0 @@
|
|||
const service = process.argv[2];
|
||||
|
||||
type ServiceOptions = {
|
||||
command: string[];
|
||||
cwd?: string;
|
||||
};
|
||||
|
||||
const services: Record<string, ServiceOptions> = {
|
||||
api: {
|
||||
command: ["fabro", "serve", "--host", "0.0.0.0"],
|
||||
},
|
||||
web: {
|
||||
command: ["bun", "run", "dev", "--host", "0.0.0.0"],
|
||||
cwd: "apps/fabro-web",
|
||||
},
|
||||
docs: {
|
||||
command: ["mintlify", "dev", "--port", "3333", "--host", "0.0.0.0"],
|
||||
cwd: "docs",
|
||||
},
|
||||
};
|
||||
|
||||
const validServices = Object.keys(services).join("|");
|
||||
|
||||
if (!service) {
|
||||
console.error(`Usage: docker run fabro <${validServices}>`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const config = services[service];
|
||||
if (!config) {
|
||||
console.error(
|
||||
`Unknown service: ${service}. Use one of: ${Object.keys(services).join(", ")}`,
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const child = Bun.spawn(config.command, {
|
||||
stdout: "inherit",
|
||||
stderr: "inherit",
|
||||
stdin: "inherit",
|
||||
cwd: config.cwd,
|
||||
});
|
||||
|
||||
process.exit(await child.exited);
|
||||
2
docker/settings.toml
Normal file
2
docker/settings.toml
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
[server.storage]
|
||||
root = "/storage"
|
||||
|
|
@ -2,53 +2,37 @@
|
|||
|
||||
Screenshots of the Fabro web UI are embedded in the public docs. This guide covers how to retake them when the UI changes.
|
||||
|
||||
> **Known gaps** after the server absorbed the web UI (April 2026):
|
||||
> - **Demo mode** is now per-request via the `X-Fabro-Demo: 1` header, not a container env var. The browser won't send it by default — you'll need a browser extension (e.g. ModHeader) or a sidecar proxy to inject the header. Needs a follow-up to land a server-level toggle.
|
||||
> - **HMR trick is gone.** Web assets are baked into the Rust binary (`fabro-spa/assets`), so the `sed` technique for hiding nav items no longer works. Either commit a temporary nav change to a local branch and rebuild, or script visibility changes in the browser devtools.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- Docker installed
|
||||
- Chrome running with DevTools MCP or similar screenshot tool
|
||||
- The `fabro` Docker image already built (`docker compose -f docker/docker-compose.yaml build`)
|
||||
- Header-injection tool (browser extension or proxy) to send `X-Fabro-Demo: 1`
|
||||
- The `fabro` Docker image built locally: `bin/dev/docker-build.sh`
|
||||
|
||||
## Boot the demo environment
|
||||
|
||||
```bash
|
||||
docker compose -f docker/docker-compose.yaml up api web -d
|
||||
docker compose -f docker/docker-compose.yaml up -d
|
||||
```
|
||||
|
||||
Wait ~10 seconds for both services to be ready, then verify:
|
||||
Wait ~5 seconds for the server to be ready, then verify (sending the demo header):
|
||||
|
||||
```bash
|
||||
curl -s -o /dev/null -w "%{http_code}" http://localhost:3000/runs
|
||||
curl -s -H "X-Fabro-Demo: 1" -o /dev/null -w "%{http_code}" http://localhost/runs
|
||||
# Should return 200
|
||||
```
|
||||
|
||||
The web container runs in demo mode (`FABRO_DEMO=1`), which returns synthetic data from the API.
|
||||
|
||||
## Applying temporary UI changes for screenshots
|
||||
|
||||
The Docker image bakes in the fabro-web source at build time. To make temporary changes (like hiding nav items), edit files inside the running container using `sed`:
|
||||
|
||||
```bash
|
||||
# Example: hide Start and Settings nav items
|
||||
docker exec docker-web-1 sed -i '/{.*"Start".*}/d; /{.*"Settings".*}/d' \
|
||||
/app/apps/fabro-web/app/layouts/app-shell.tsx
|
||||
```
|
||||
|
||||
**Do not use `docker cp`** to replace source files — it breaks Vite's module resolution and causes 500 errors. Use `sed -i` inside the container instead, which preserves the file inode and lets HMR work correctly.
|
||||
|
||||
`docker cp` works fine for static assets in `public/` (logos, images), just not for source files that Vite processes.
|
||||
|
||||
After `sed` edits, wait a few seconds for HMR to rebuild, then verify pages still return 200 before taking screenshots.
|
||||
With the SPA baked into the binary, there's no in-container edit path. Commit the nav change on a local branch, rerun `bin/dev/docker-build.sh`, then `docker compose up -d --force-recreate`.
|
||||
|
||||
## Updating logos
|
||||
|
||||
The logos are static files in `apps/fabro-web/public/`. The Docker compose file mounts this directory as a volume:
|
||||
|
||||
```yaml
|
||||
volumes:
|
||||
- ../apps/fabro-web/public:/app/apps/fabro-web/public
|
||||
```
|
||||
|
||||
So changes to `apps/fabro-web/public/logotype.svg` (dark theme) and `apps/fabro-web/public/logotype-light.svg` (light theme) are picked up immediately by the running container. The source-of-truth logos are in `docs/logo/dark.svg` and `docs/logo/light.svg`.
|
||||
Logos live at `apps/fabro-web/public/logotype.svg` (dark) and `apps/fabro-web/public/logotype-light.svg` (light). These are bundled into the SPA at build time — rebuilding the image picks up the changes. The source-of-truth logos are in `docs/logo/dark.svg` and `docs/logo/light.svg`.
|
||||
|
||||
## Browser setup
|
||||
|
||||
|
|
@ -61,7 +45,7 @@ If the nav bar is too crowded at this width, hide low-priority items (Start, Set
|
|||
|
||||
## Taking screenshots
|
||||
|
||||
Screenshots live in `docs/images/web/`. Each screenshot maps to a specific URL:
|
||||
Screenshots live in `docs/images/web/`. Each screenshot maps to a specific URL (served from `http://localhost/` with the `X-Fabro-Demo: 1` header):
|
||||
|
||||
| File | URL |
|
||||
|---|---|
|
||||
|
|
@ -91,7 +75,6 @@ Screenshots live in `docs/images/web/`. Each screenshot maps to a specific URL:
|
|||
### Pages that need extra wait time
|
||||
|
||||
- **Run overview** (`/runs/run-1`) — the workflow graph diagram takes 2-3 seconds to render after the page loads. Wait before screenshotting.
|
||||
- **After `sed` edits** — HMR needs a few seconds to rebuild. The first navigation after an edit may hit a transient error; retry once.
|
||||
|
||||
## Where screenshots are used in docs
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue