irongit

Git hosting and a container registry in one Rust binary (axum + Astro)

Docker image and deploy script for the rybbit server (git.hygo.ai)

huncholanehuncholaneauthored
parent 7f2479dcommit 77024a62e576a2fbdfaef25ec8af0656f35577f5Browse files

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"