mirror of
https://github.com/kharonsec/br-acc
synced 2026-04-25 17:15:02 +02:00
feat(ci): add docker-ci workflow for GHCR and optional image-based deploy (#47)
* feat(ci): add docker-ci workflow for Buildx and GHCR push * refactor(docker): use per-service builds in root docker-compose * refactor(etl): use uv and uv.lock in ETL Dockerfile * fix(api): add uv.lock to API Dockerfile for reproducible builds * feat(deploy): add optional GHCR image pull and prod images override * refactor(docker): use uv in root Dockerfile etl stage, document canonical Dockerfiles * chore(docker): extend .dockerignore for build context * docs: add Docker Compose start option to README
This commit is contained in:
@@ -23,6 +23,10 @@
|
||||
data
|
||||
docs
|
||||
audit-results
|
||||
scripts
|
||||
**/tests
|
||||
**/test_*
|
||||
**/*.md
|
||||
|
||||
.vscode
|
||||
.idea
|
||||
|
||||
87
.github/workflows/docker-ci.yml
vendored
Normal file
87
.github/workflows/docker-ci.yml
vendored
Normal file
@@ -0,0 +1,87 @@
|
||||
# Build and push Docker images to GHCR (repo_servicename, Buildx).
|
||||
# Runs on push to main and on version tags (v*).
|
||||
name: Docker CI
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
tags: ["v*"]
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
|
||||
env:
|
||||
REGISTRY: ghcr.io
|
||||
# Image name prefix: owner/repo_servicename (e.g. World-Open-Graph/br-acc_api)
|
||||
IMAGE_PREFIX: ${{ github.repository_owner }}/${{ github.event.repository.name }}
|
||||
|
||||
jobs:
|
||||
build-push:
|
||||
name: Build and push images
|
||||
runs-on: ubuntu-latest
|
||||
if: github.event_name == 'push' && (github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/'))
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Log in to GHCR
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ${{ env.REGISTRY }}
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Docker meta (tags)
|
||||
id: meta
|
||||
run: |
|
||||
SHORT_SHA=$(git rev-parse --short HEAD)
|
||||
if [ "${{ github.ref }}" = "refs/heads/main" ]; then
|
||||
echo "tag1=sha-${SHORT_SHA}" >> "$GITHUB_OUTPUT"
|
||||
elif [[ "${{ github.ref }}" == refs/tags/* ]]; then
|
||||
echo "tag1=${{ github.ref_name }}" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
echo "tag2=latest" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Build and push API
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: api
|
||||
file: api/Dockerfile
|
||||
push: true
|
||||
tags: |
|
||||
${{ env.REGISTRY }}/${{ env.IMAGE_PREFIX }}_api:${{ steps.meta.outputs.tag1 }}
|
||||
${{ env.REGISTRY }}/${{ env.IMAGE_PREFIX }}_api:${{ steps.meta.outputs.tag2 }}
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha,mode=max
|
||||
|
||||
- name: Build and push Frontend
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: frontend
|
||||
file: frontend/Dockerfile
|
||||
push: true
|
||||
tags: |
|
||||
${{ env.REGISTRY }}/${{ env.IMAGE_PREFIX }}_frontend:${{ steps.meta.outputs.tag1 }}
|
||||
${{ env.REGISTRY }}/${{ env.IMAGE_PREFIX }}_frontend:${{ steps.meta.outputs.tag2 }}
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha,mode=max
|
||||
|
||||
- name: Build and push ETL
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: etl
|
||||
file: etl/Dockerfile
|
||||
push: true
|
||||
tags: |
|
||||
${{ env.REGISTRY }}/${{ env.IMAGE_PREFIX }}_etl:${{ steps.meta.outputs.tag1 }}
|
||||
${{ env.REGISTRY }}/${{ env.IMAGE_PREFIX }}_etl:${{ steps.meta.outputs.tag2 }}
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha,mode=max
|
||||
11
Dockerfile
11
Dockerfile
@@ -1,3 +1,4 @@
|
||||
# Multi-stage Dockerfile (root). Prefer per-service Dockerfiles: api/Dockerfile, frontend/Dockerfile, etl/Dockerfile.
|
||||
FROM python:3.12-slim AS api
|
||||
|
||||
WORKDIR /app
|
||||
@@ -44,11 +45,13 @@ ENV PYTHONDONTWRITEBYTECODE=1 \
|
||||
|
||||
WORKDIR /workspace/etl
|
||||
|
||||
COPY etl/pyproject.toml ./
|
||||
COPY etl/src ./src
|
||||
COPY --from=ghcr.io/astral-sh/uv:latest /uv /usr/local/bin/uv
|
||||
|
||||
RUN python -m pip install --upgrade pip \
|
||||
&& python -m pip install .
|
||||
COPY etl/pyproject.toml etl/uv.lock ./
|
||||
RUN uv sync --no-dev --no-install-project
|
||||
|
||||
COPY etl/src ./src
|
||||
RUN uv sync --no-dev
|
||||
|
||||
WORKDIR /workspace
|
||||
CMD ["bash"]
|
||||
|
||||
17
README.md
17
README.md
@@ -55,6 +55,23 @@ Verify with:
|
||||
- Frontend: http://localhost:3000
|
||||
- Neo4j Browser: http://localhost:7474
|
||||
|
||||
### Starting with Docker
|
||||
|
||||
You can start the stack (Neo4j, API, frontend) with Docker Compose without running the full bootstrap:
|
||||
|
||||
```bash
|
||||
cp .env.example .env
|
||||
docker compose up -d
|
||||
```
|
||||
|
||||
Optional: include the ETL service (for running pipelines in a container):
|
||||
|
||||
```bash
|
||||
docker compose --profile etl up -d
|
||||
```
|
||||
|
||||
Same verification URLs apply. For a ready-to-use demo graph with seed data, use `make bootstrap-demo` instead.
|
||||
|
||||
---
|
||||
|
||||
## One-Command Flow
|
||||
|
||||
@@ -8,7 +8,7 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
libpango-1.0-0 libpangocairo-1.0-0 libgdk-pixbuf-2.0-0 libcairo2 libffi-dev \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
COPY pyproject.toml .
|
||||
COPY pyproject.toml uv.lock .
|
||||
RUN uv sync --no-dev --no-install-project
|
||||
|
||||
COPY src/ src/
|
||||
|
||||
@@ -23,10 +23,7 @@ services:
|
||||
retries: 8
|
||||
|
||||
api:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile
|
||||
target: api
|
||||
build: ./api
|
||||
ports:
|
||||
- "8000:8000"
|
||||
environment:
|
||||
@@ -49,10 +46,7 @@ services:
|
||||
start_period: 20s
|
||||
|
||||
frontend:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile
|
||||
target: frontend
|
||||
build: ./frontend
|
||||
ports:
|
||||
- "3000:3000"
|
||||
environment:
|
||||
@@ -62,10 +56,7 @@ services:
|
||||
condition: service_healthy
|
||||
|
||||
etl:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile
|
||||
target: etl
|
||||
build: ./etl
|
||||
profiles: ["etl"]
|
||||
working_dir: /workspace
|
||||
environment:
|
||||
|
||||
@@ -55,6 +55,23 @@ Verifique em:
|
||||
- Frontend: http://localhost:3000
|
||||
- Neo4j Browser: http://localhost:7474
|
||||
|
||||
### Subir com Docker
|
||||
|
||||
Voce pode subir a stack (Neo4j, API, frontend) com Docker Compose sem rodar o bootstrap completo:
|
||||
|
||||
```bash
|
||||
cp .env.example .env
|
||||
docker compose up -d
|
||||
```
|
||||
|
||||
Opcional: incluir o servico ETL (para rodar pipelines no container):
|
||||
|
||||
```bash
|
||||
docker compose --profile etl up -d
|
||||
```
|
||||
|
||||
As mesmas URLs de verificacao valem. Para um grafo demo pronto com dados de seed, use `make bootstrap-demo`.
|
||||
|
||||
---
|
||||
|
||||
## Fluxo Em Um Comando
|
||||
|
||||
@@ -6,11 +6,13 @@ ENV PYTHONDONTWRITEBYTECODE=1 \
|
||||
|
||||
WORKDIR /workspace/etl
|
||||
|
||||
COPY pyproject.toml ./
|
||||
COPY src ./src
|
||||
COPY --from=ghcr.io/astral-sh/uv:latest /uv /usr/local/bin/uv
|
||||
|
||||
RUN python -m pip install --upgrade pip \
|
||||
&& python -m pip install .
|
||||
COPY pyproject.toml uv.lock ./
|
||||
RUN uv sync --no-dev --no-install-project
|
||||
|
||||
COPY src ./src
|
||||
RUN uv sync --no-dev
|
||||
|
||||
WORKDIR /workspace
|
||||
CMD ["bash"]
|
||||
|
||||
11
infra/docker-compose.prod.images.yml
Normal file
11
infra/docker-compose.prod.images.yml
Normal file
@@ -0,0 +1,11 @@
|
||||
# Override for production when using pre-built images from GHCR.
|
||||
# Set REGISTRY_IMAGE_PREFIX (e.g. ghcr.io/owner/br-acc) and REGISTRY_IMAGE_TAG (e.g. latest or sha-xxx).
|
||||
# Use with: COMPOSE_FILE=infra/docker-compose.prod.yml:infra/docker-compose.prod.images.yml
|
||||
services:
|
||||
api:
|
||||
image: ${REGISTRY_IMAGE_PREFIX}_api:${REGISTRY_IMAGE_TAG:-latest}
|
||||
build: ~
|
||||
|
||||
frontend:
|
||||
image: ${REGISTRY_IMAGE_PREFIX}_frontend:${REGISTRY_IMAGE_TAG:-latest}
|
||||
build: ~
|
||||
@@ -2,7 +2,13 @@
|
||||
set -euo pipefail
|
||||
|
||||
DEPLOY_DIR="${DEPLOY_DIR:-/opt/bracc}"
|
||||
COMPOSE_FILE="$DEPLOY_DIR/infra/docker-compose.prod.yml"
|
||||
COMPOSE_BASE="$DEPLOY_DIR/infra/docker-compose.prod.yml"
|
||||
# When USE_GHCR_IMAGES=true, use override that pulls images from GHCR (set REGISTRY_IMAGE_PREFIX and optional REGISTRY_IMAGE_TAG)
|
||||
if [ "${USE_GHCR_IMAGES:-false}" = "true" ]; then
|
||||
COMPOSE_FILE="${COMPOSE_BASE}:${DEPLOY_DIR}/infra/docker-compose.prod.images.yml"
|
||||
else
|
||||
COMPOSE_FILE="$COMPOSE_BASE"
|
||||
fi
|
||||
DRY_RUN=false
|
||||
|
||||
for arg in "$@"; do
|
||||
@@ -24,25 +30,50 @@ log "Deploying BR-ACC..."
|
||||
|
||||
cd "$DEPLOY_DIR"
|
||||
|
||||
log "Pulling latest changes..."
|
||||
if [ "$DRY_RUN" = true ]; then
|
||||
if [ "${USE_GHCR_IMAGES:-false}" = "true" ]; then
|
||||
if [ -z "${REGISTRY_IMAGE_PREFIX:-}" ]; then
|
||||
echo "Error: REGISTRY_IMAGE_PREFIX required when USE_GHCR_IMAGES=true (e.g. ghcr.io/owner/br-acc)" >&2
|
||||
exit 1
|
||||
fi
|
||||
REGISTRY_IMAGE_TAG="${REGISTRY_IMAGE_TAG:-latest}"
|
||||
export REGISTRY_IMAGE_PREFIX REGISTRY_IMAGE_TAG
|
||||
log "Using GHCR images: ${REGISTRY_IMAGE_PREFIX}_api:${REGISTRY_IMAGE_TAG} (and frontend)"
|
||||
log "Logging in to GHCR..."
|
||||
if [ "$DRY_RUN" = true ]; then
|
||||
log "[DRY RUN] Would run: echo \$GHCR_TOKEN | docker login ghcr.io -u USER --password-stdin"
|
||||
else
|
||||
echo "${GHCR_TOKEN:?Set GHCR_TOKEN for GHCR login}" | docker login ghcr.io -u "${GHCR_USER:?Set GHCR_USER for GHCR login}" --password-stdin
|
||||
fi
|
||||
log "Pulling images..."
|
||||
if [ "$DRY_RUN" = true ]; then
|
||||
log "[DRY RUN] Would run: docker compose pull"
|
||||
else
|
||||
docker compose -f "$COMPOSE_BASE" -f "$DEPLOY_DIR/infra/docker-compose.prod.images.yml" pull api frontend
|
||||
fi
|
||||
else
|
||||
log "Pulling latest changes..."
|
||||
if [ "$DRY_RUN" = true ]; then
|
||||
log "[DRY RUN] Would run: git pull origin main"
|
||||
else
|
||||
else
|
||||
git pull origin main
|
||||
fi
|
||||
|
||||
log "Building containers..."
|
||||
if [ "$DRY_RUN" = true ]; then
|
||||
fi
|
||||
log "Building containers..."
|
||||
if [ "$DRY_RUN" = true ]; then
|
||||
log "[DRY RUN] Would run: docker compose build"
|
||||
else
|
||||
else
|
||||
docker compose -f "$COMPOSE_FILE" build
|
||||
fi
|
||||
fi
|
||||
|
||||
log "Starting services..."
|
||||
if [ "$DRY_RUN" = true ]; then
|
||||
log "[DRY RUN] Would run: docker compose up -d"
|
||||
log "[DRY RUN] Would run: docker compose up -d"
|
||||
else
|
||||
if [ "${USE_GHCR_IMAGES:-false}" = "true" ]; then
|
||||
docker compose -f "$COMPOSE_BASE" -f "$DEPLOY_DIR/infra/docker-compose.prod.images.yml" up -d
|
||||
else
|
||||
docker compose -f "$COMPOSE_FILE" up -d
|
||||
fi
|
||||
fi
|
||||
|
||||
log "Waiting for health check..."
|
||||
@@ -53,7 +84,11 @@ if [ "$DRY_RUN" = false ]; then
|
||||
log "Health check passed ($HEALTH_URL)."
|
||||
else
|
||||
log "Health check failed ($HEALTH_URL)!"
|
||||
docker compose -f "$COMPOSE_FILE" logs --tail=50
|
||||
if [ "${USE_GHCR_IMAGES:-false}" = "true" ]; then
|
||||
docker compose -f "$COMPOSE_BASE" -f "$DEPLOY_DIR/infra/docker-compose.prod.images.yml" logs --tail=50
|
||||
else
|
||||
docker compose -f "$COMPOSE_FILE" logs --tail=50
|
||||
fi
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
Reference in New Issue
Block a user