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:
Davi Rezende
2026-03-03 21:26:32 -03:00
committed by GitHub
parent d265df24a1
commit d889569a78
10 changed files with 199 additions and 32 deletions

View File

@@ -23,6 +23,10 @@
data
docs
audit-results
scripts
**/tests
**/test_*
**/*.md
.vscode
.idea

87
.github/workflows/docker-ci.yml vendored Normal file
View 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

View File

@@ -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"]

View File

@@ -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

View File

@@ -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/

View File

@@ -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:

View File

@@ -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

View File

@@ -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"]

View 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: ~

View File

@@ -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