Git hosting and a container registry in one Rust binary (axum + Astro)
Docker image and deploy script for the rybbit server (git.hygo.ai)
3 files changed, +140 -0
+12-0.dockerignore
| @@ -0,0 +1,12 @@ | ||
| 1 | +target/ | |
| 2 | +data/ | |
| 3 | +gitea/ | |
| 4 | +.git/ | |
| 5 | +.claude/ | |
| 6 | +.playwright-mcp/ | |
| 7 | +frontend/node_modules/ | |
| 8 | +frontend/dist/ | |
| 9 | +frontend/.astro/ | |
| 10 | +.env | |
| 11 | +.env.* | |
| 12 | +secrets.md |
+66-0Dockerfile
| @@ -0,0 +1,66 @@ | ||
| 1 | +# syntax=docker/dockerfile:1.7 | |
| 2 | +# irongit: server (axum + embedded Astro build) plus the static ig CLI, which | |
| 3 | +# the server publishes to R2 for install.sh and `ig upgrade`. | |
| 4 | +# docker build -t irongit . | |
| 5 | + | |
| 6 | +# ---- Frontend ----------------------------------------------------------------- | |
| 7 | +FROM oven/bun:1.4 AS frontend | |
| 8 | +WORKDIR /src/frontend | |
| 9 | +COPY frontend/package.json frontend/bun.lock ./ | |
| 10 | +RUN bun install --frozen-lockfile | |
| 11 | +COPY frontend/ ./ | |
| 12 | +# The backend's Rust templates use Tailwind classes too; the CSS build scans them. | |
| 13 | +COPY backend/src /src/backend/src | |
| 14 | +RUN bun run build | |
| 15 | + | |
| 16 | +# ---- Rust --------------------------------------------------------------------- | |
| 17 | +FROM rust:1-trixie AS rust | |
| 18 | +RUN apt-get update \ | |
| 19 | + && apt-get install -y --no-install-recommends musl-tools \ | |
| 20 | + && rm -rf /var/lib/apt/lists/* \ | |
| 21 | + && rustup target add x86_64-unknown-linux-musl | |
| 22 | +WORKDIR /src | |
| 23 | +COPY Cargo.toml Cargo.lock ./ | |
| 24 | +COPY backend/ backend/ | |
| 25 | +COPY cli/ cli/ | |
| 26 | +COPY shared/ shared/ | |
| 27 | +COPY --from=frontend /src/frontend/dist frontend/dist | |
| 28 | +# Cache mounts keep the registry and build artifacts between image builds; | |
| 29 | +# the binaries are copied out of the cache within the same step. | |
| 30 | +RUN --mount=type=cache,target=/usr/local/cargo/registry \ | |
| 31 | + --mount=type=cache,target=/src/target \ | |
| 32 | + cargo build --release --locked -p irongit --no-default-features \ | |
| 33 | + && cargo build --release --locked -p ig --target x86_64-unknown-linux-musl \ | |
| 34 | + && mkdir -p /out \ | |
| 35 | + && cp target/release/irongit /out/irongit \ | |
| 36 | + && cp target/x86_64-unknown-linux-musl/release/ig /out/ig | |
| 37 | + | |
| 38 | +# ---- Runtime ------------------------------------------------------------------ | |
| 39 | +FROM debian:trixie-slim AS runtime | |
| 40 | +# git serves every push, clone and page; postgresql-client (17) is for backups. | |
| 41 | +RUN apt-get update \ | |
| 42 | + && apt-get install -y --no-install-recommends ca-certificates git postgresql-client wget \ | |
| 43 | + && rm -rf /var/lib/apt/lists/* \ | |
| 44 | + && groupadd --system --gid 10001 irongit \ | |
| 45 | + && useradd --system --uid 10001 --gid irongit --home-dir /data --no-create-home --shell /usr/sbin/nologin irongit \ | |
| 46 | + && mkdir -p /data \ | |
| 47 | + && chown irongit:irongit /data \ | |
| 48 | + && chmod 0750 /data | |
| 49 | + | |
| 50 | +COPY --from=rust --chown=root:root --chmod=0755 /out/irongit /usr/local/bin/irongit | |
| 51 | +COPY --from=rust --chown=root:root --chmod=0755 /out/ig /usr/local/share/irongit/ig | |
| 52 | + | |
| 53 | +USER irongit | |
| 54 | +ENV HOST=0.0.0.0 \ | |
| 55 | + PORT=7878 \ | |
| 56 | + SSH_PORT=2222 \ | |
| 57 | + DATA_DIR=/data \ | |
| 58 | + HOME=/data \ | |
| 59 | + MALLOC_ARENA_MAX=2 \ | |
| 60 | + RUST_LOG=info | |
| 61 | +EXPOSE 7878 2222 | |
| 62 | +VOLUME ["/data"] | |
| 63 | +HEALTHCHECK --interval=10s --timeout=5s --start-period=20s --retries=6 \ | |
| 64 | + CMD wget -q -O /dev/null http://127.0.0.1:7878/api/health || exit 1 | |
| 65 | +ENTRYPOINT ["/usr/local/bin/irongit"] | |
| 66 | +CMD ["serve"] |
+62-0deploy.sh
| @@ -0,0 +1,62 @@ | ||
| 1 | +#!/usr/bin/env bash | |
| 2 | +# Deploy irongit to the rybbit server: build the image here, ship it over SSH, | |
| 3 | +# and roll only the irongit service in /opt/hygo (behind Caddy, on the shared | |
| 4 | +# Postgres). Publishes the bundled ig CLI to R2 when its version changed. | |
| 5 | +# | |
| 6 | +# ./deploy.sh | |
| 7 | +# | |
| 8 | +# Roll back to the previous image: | |
| 9 | +# ssh rybbit 'cd /opt/hygo && docker tag irongit:$(cat .irongit.prev) irongit:latest && docker compose up -d --no-deps irongit' | |
| 10 | +set -euo pipefail | |
| 11 | + | |
| 12 | +cd "$(dirname "$0")" | |
| 13 | +REMOTE=${DEPLOY_HOST:-rybbit} | |
| 14 | +TAG=$(git rev-parse --short HEAD) | |
| 15 | +if ! git diff --quiet HEAD; then | |
| 16 | + TAG="$TAG-dirty-$(date +%Y%m%d%H%M%S)" | |
| 17 | +fi | |
| 18 | +IG_VERSION=$(sed -n 's/^version = "\(.*\)"/\1/p' cli/Cargo.toml | head -1) | |
| 19 | + | |
| 20 | +echo "==> building irongit:$TAG (ig $IG_VERSION)" | |
| 21 | +docker build -t "irongit:$TAG" . | |
| 22 | + | |
| 23 | +echo "==> shipping to $REMOTE" | |
| 24 | +docker save "irongit:$TAG" | gzip -1 | ssh "$REMOTE" 'gunzip | docker load' | |
| 25 | + | |
| 26 | +echo "==> rolling" | |
| 27 | +ssh "$REMOTE" bash -s -- "$TAG" "$IG_VERSION" <<'REMOTE_SCRIPT' | |
| 28 | +set -euo pipefail | |
| 29 | +cd /opt/hygo | |
| 30 | +tag=$1 | |
| 31 | +ig_version=$2 | |
| 32 | + | |
| 33 | +[ -f .irongit.tag ] && cp .irongit.tag .irongit.prev | |
| 34 | +docker tag "irongit:$tag" irongit:latest | |
| 35 | +echo "$tag" > .irongit.tag | |
| 36 | +docker compose up -d --no-deps irongit | |
| 37 | + | |
| 38 | +ok=0 | |
| 39 | +for _ in $(seq 1 45); do | |
| 40 | + status=$(docker inspect irongit --format '{{.State.Health.Status}}' 2>/dev/null || echo none) | |
| 41 | + if [ "$status" = healthy ]; then ok=1; break; fi | |
| 42 | + sleep 2 | |
| 43 | +done | |
| 44 | +if [ "$ok" != 1 ]; then | |
| 45 | + echo "irongit did not become healthy; last logs:" >&2 | |
| 46 | + docker logs --tail 60 irongit >&2 | |
| 47 | + exit 1 | |
| 48 | +fi | |
| 49 | +echo "irongit:$tag healthy" | |
| 50 | + | |
| 51 | +published=$(docker exec irongit wget -qO- http://127.0.0.1:7878/api/v1/cli/latest 2>/dev/null | sed -n 's/.*"version":"\([^"]*\)".*/\1/p' || true) | |
| 52 | +if [ "$published" != "$ig_version" ]; then | |
| 53 | + echo "publishing ig $ig_version (was ${published:-none})" | |
| 54 | + docker exec irongit irongit admin publish-cli /usr/local/share/irongit/ig --version "$ig_version" | |
| 55 | +fi | |
| 56 | + | |
| 57 | +# Keep the three newest irongit images. | |
| 58 | +docker images irongit --format '{{.Tag}} {{.CreatedAt}}' | grep -v '^latest ' | sort -k2 -r | tail -n +4 | cut -d' ' -f1 \ | |
| 59 | + | while read -r old; do [ "$old" = "$tag" ] || docker rmi "irongit:$old" >/dev/null 2>&1 || true; done | |
| 60 | +REMOTE_SCRIPT | |
| 61 | + | |
| 62 | +echo "==> done: https://git.hygo.ai" |