diff --git a/.github/workflows/build-test-push-synvya.yaml b/.github/workflows/build-test-push-synvya.yaml index c885b1b8..cdd0721f 100644 --- a/.github/workflows/build-test-push-synvya.yaml +++ b/.github/workflows/build-test-push-synvya.yaml @@ -144,15 +144,23 @@ jobs: exit 1 fi git pull --ff-only origin synvya-staging + bash scripts/ec2-prepare-host.sh bash scripts/load-secrets.sh staging if [ -n "${{ vars.DISABLE_EMAILS }}" ]; then echo "DISABLE_EMAILS=${{ vars.DISABLE_EMAILS }}" >> /opt/synvya/.env fi docker system df || true - docker builder prune -af || true - docker image prune -af || true + # Light cleanup: drop dangling images and unreferenced build + # cache only. `-a` would wipe referenced layers and force a + # cold cargo build of all dependencies on every deploy. + docker image prune -f || true + docker builder prune -f || true + # Force BuildKit so the Dockerfile's --mount=type=cache works. + # Compose v2 enables it by default, but be explicit. + export DOCKER_BUILDKIT=1 + export COMPOSE_DOCKER_CLI_BUILD=1 docker compose -f docker-compose.synvya.yml --env-file /opt/synvya/.env \ - build postgres redis migrate keycast + build --build-arg CARGO_BUILD_JOBS=2 postgres redis migrate keycast docker compose -f docker-compose.synvya.yml --env-file /opt/synvya/.env \ up -d postgres redis migrate keycast sleep 10 @@ -182,15 +190,23 @@ jobs: exit 1 fi git pull --ff-only origin synvya + bash scripts/ec2-prepare-host.sh bash scripts/load-secrets.sh production if [ -n "${{ vars.DISABLE_EMAILS }}" ]; then echo "DISABLE_EMAILS=${{ vars.DISABLE_EMAILS }}" >> /opt/synvya/.env fi docker system df || true - docker builder prune -af || true - docker image prune -af || true + # Light cleanup: drop dangling images and unreferenced build + # cache only. `-a` would wipe referenced layers and force a + # cold cargo build of all dependencies on every deploy. + docker image prune -f || true + docker builder prune -f || true + # Force BuildKit so the Dockerfile's --mount=type=cache works. + # Compose v2 enables it by default, but be explicit. + export DOCKER_BUILDKIT=1 + export COMPOSE_DOCKER_CLI_BUILD=1 docker compose -f docker-compose.synvya.yml --env-file /opt/synvya/.env \ - build postgres redis migrate keycast + build --build-arg CARGO_BUILD_JOBS=2 postgres redis migrate keycast docker compose -f docker-compose.synvya.yml --env-file /opt/synvya/.env \ up -d postgres redis migrate keycast sleep 10 @@ -223,6 +239,9 @@ jobs: fi source "$HOME/.cargo/env" + # Ensure swap + cargo jobs limit are in place for the QA build + bash /opt/synvya/keycast/scripts/ec2-prepare-host.sh + # Load env for POSTGRES_PASSWORD set -a source /opt/synvya/.env 2>/dev/null || true @@ -301,6 +320,9 @@ jobs: fi source "$HOME/.cargo/env" + # Ensure swap + cargo jobs limit are in place for the QA build + bash /opt/synvya/keycast/scripts/ec2-prepare-host.sh + # Load env for POSTGRES_PASSWORD set -a source /opt/synvya/.env 2>/dev/null || true diff --git a/Dockerfile b/Dockerfile index 3334f2bd..066778ff 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,3 +1,4 @@ +# syntax=docker/dockerfile:1.4 # Build stage for Rust API FROM rust:1.93-slim AS rust-builder @@ -18,12 +19,27 @@ COPY ./Cargo.toml ./Cargo.toml COPY ./Cargo.lock ./Cargo.lock ARG CARGO_FEATURES="" -RUN if [ -n "$CARGO_FEATURES" ]; then \ +ARG CARGO_BUILD_JOBS="" +ENV CARGO_BUILD_JOBS=${CARGO_BUILD_JOBS} + +# BuildKit cache mounts persist the cargo registry/git index and the +# target/ directory across builds on the same host. Unchanged crates +# stay compiled, so warm builds skip the ~400-crate dependency build. +# Artifacts must be copied out of target/ before the RUN ends because +# cache mounts are not part of the resulting image layer. +RUN --mount=type=cache,target=/usr/local/cargo/registry,sharing=locked \ + --mount=type=cache,target=/usr/local/cargo/git,sharing=locked \ + --mount=type=cache,target=/app/target,sharing=locked \ + set -e; \ + if [ -n "$CARGO_FEATURES" ]; then \ cargo build --release --bin keycast --features "$CARGO_FEATURES"; \ else \ cargo build --release --bin keycast; \ - fi -RUN cargo build --release --example migrate-vine-users + fi; \ + cargo build --release --example migrate-vine-users; \ + mkdir -p /artifacts; \ + cp target/release/keycast /artifacts/keycast; \ + cp target/release/examples/migrate-vine-users /artifacts/migrate-vine-users # Build stage for Bun frontend FROM oven/bun:1 AS web-builder @@ -109,9 +125,11 @@ RUN curl -fsSL https://bun.sh/install | bash # Create necessary directories RUN mkdir -p /app/database /data -# Copy built artifacts - keycast binary and migration tool -COPY --from=rust-builder /app/target/release/keycast ./ -COPY --from=rust-builder /app/target/release/examples/migrate-vine-users ./ +# Copy built artifacts - keycast binary and migration tool. +# Sources are /artifacts/ (not target/) because target/ is a BuildKit +# cache mount in rust-builder and is not present in its image layer. +COPY --from=rust-builder /artifacts/keycast ./ +COPY --from=rust-builder /artifacts/migrate-vine-users ./ COPY --from=web-builder /app/web/build ./web COPY --from=web-builder /app/web/package.json ./ COPY --from=web-builder /app/web/node_modules ./node_modules diff --git a/scripts/ec2-prepare-host.sh b/scripts/ec2-prepare-host.sh new file mode 100755 index 00000000..43962ecf --- /dev/null +++ b/scripts/ec2-prepare-host.sh @@ -0,0 +1,43 @@ +#!/usr/bin/env bash +# Idempotent host prep for low-memory EC2 instances (e.g. t3.medium). +# Ensures enough swap to survive Rust release builds and constrains host-side +# cargo to a safe job count. Safe to run on every deploy. +set -euo pipefail + +SWAPFILE="${SWAPFILE:-/swapfile}" +SWAP_SIZE="${SWAP_SIZE:-4G}" +CARGO_JOBS="${CARGO_JOBS:-2}" + +echo "=== ec2-prepare-host: ensure swap (${SWAP_SIZE} at ${SWAPFILE}) ===" +if swapon --show=NAME --noheadings | grep -qx "${SWAPFILE}"; then + echo "swap already active at ${SWAPFILE}" +else + if [ ! -f "${SWAPFILE}" ]; then + sudo fallocate -l "${SWAP_SIZE}" "${SWAPFILE}" || \ + sudo dd if=/dev/zero of="${SWAPFILE}" bs=1M count=$((${SWAP_SIZE%G} * 1024)) + sudo chmod 600 "${SWAPFILE}" + sudo mkswap "${SWAPFILE}" + fi + sudo swapon "${SWAPFILE}" + if ! grep -qE "^${SWAPFILE}\s" /etc/fstab; then + echo "${SWAPFILE} none swap sw 0 0" | sudo tee -a /etc/fstab >/dev/null + fi + echo "swap enabled at ${SWAPFILE}" +fi + +echo "=== ec2-prepare-host: ensure ~/.cargo/config.toml jobs=${CARGO_JOBS} ===" +mkdir -p "${HOME}/.cargo" +CARGO_CFG="${HOME}/.cargo/config.toml" +if [ ! -f "${CARGO_CFG}" ] || ! grep -qE '^\[build\]' "${CARGO_CFG}"; then + cat >> "${CARGO_CFG}" <