diff --git a/.github/workflows/backend-docker-publish.yml b/.github/workflows/backend-docker-publish.yml index b8c8e58..aa8b9ec 100644 --- a/.github/workflows/backend-docker-publish.yml +++ b/.github/workflows/backend-docker-publish.yml @@ -190,6 +190,15 @@ jobs: environment: production steps: + - name: Checkout compose files + uses: actions/checkout@v6 + with: + ref: backend/v${{ needs.validate.outputs.version }} + sparse-checkout: | + apps/backend/docker-compose.yml + apps/backend/docker-compose.prod.yml + sparse-checkout-cone-mode: false + - name: Connect to Tailscale uses: tailscale/github-action@v4 with: @@ -233,6 +242,16 @@ jobs: ssh "${SSH_OPTS[@]}" "$SSH_USERNAME@$TARGET_HOST" \ 'echo "SSH auth OK"; echo "Remote host: $(hostname)"' + # The release's compose files replace the server's copies, so the + # server only keeps .env. They are uploaded next to the current files + # and only take their place once the remote script validates them. + echo "Uploading compose files from backend/v$IMAGE_TAG..." + for file in docker-compose.yml docker-compose.prod.yml; do + scp -i "$KEY_FILE" -P "$SSH_PORT" \ + -o BatchMode=yes -o IdentitiesOnly=yes -o StrictHostKeyChecking=yes \ + "apps/backend/$file" "$SSH_USERNAME@$TARGET_HOST:$DEPLOY_PATH/$file.new" + done + printf -v REMOTE_IMAGE_TAG '%q' "$IMAGE_TAG" printf -v REMOTE_DEPLOY_PATH '%q' "$DEPLOY_PATH" @@ -269,13 +288,32 @@ jobs: cd "$DEPLOY_PATH" echo "Current directory: $(pwd)" - for file in docker-compose.yml docker-compose.prod.yml .env; do + for file in docker-compose.yml.new docker-compose.prod.yml.new .env; do if [ ! -f "$file" ]; then echo "::error::Required deploy file is missing: $file" exit 1 fi done + # Compose only warns about variables missing from .env and then uses + # an empty value, so treat that warning as a failure too. + echo "Validating the release's compose files against .env..." + if ! CHECK_OUTPUT="$(docker compose -f docker-compose.yml.new -f docker-compose.prod.yml.new config -q 2>&1)" \ + || grep -q 'variable is not set' <<<"$CHECK_OUTPUT"; then + printf '%s\n' "$CHECK_OUTPUT" + rm -f docker-compose.yml.new docker-compose.prod.yml.new + echo "::error::The release's compose files do not work with the server's .env. The current compose files were left in place." + exit 1 + fi + + for file in docker-compose.yml docker-compose.prod.yml; do + if [ -f "$file" ]; then + cp -p "$file" "$file.prev" + fi + mv "$file.new" "$file" + done + echo "Compose files updated from the release (previous versions kept as *.prev)" + echo "Compose services:" docker compose "${COMPOSE_FILES[@]}" config --services diff --git a/docs/deployment.md b/docs/deployment.md index 8189249..c263648 100644 --- a/docs/deployment.md +++ b/docs/deployment.md @@ -74,7 +74,8 @@ All publish workflows build from the release tag (`/v`), first che ### Backend 1. Builds `gustmrg/bitfinance-backend` for `linux/amd64` and `linux/arm64` and pushes it to Docker Hub as `` and `latest`. -2. On the server, in `DEPLOY_PATH`, it uses `docker-compose.yml` and `docker-compose.prod.yml` to: +2. Uploads `apps/backend/docker-compose.yml` and `docker-compose.prod.yml` from the release tag to `DEPLOY_PATH`, checks them with `docker compose config` against the server's `.env`, and only then puts them in place of the current files, which are kept as `*.prev`. The deploy stops before touching anything if the check fails or reports a variable that is not set. +3. On the server, in `DEPLOY_PATH`, it uses those compose files to: - pull the `bitfinance-api` image with `IMAGE_TAG=`; - apply database migrations with `docker compose run --rm --no-deps bitfinance-api --migrate`; - recreate the `bitfinance-api` container from the new image. @@ -92,6 +93,8 @@ The deploy prints the service status and recent logs, including when it fails. The backend and the MCP server share the compose project in `apps/backend` (`docker-compose.prod.yml` defines both services) and the same `DEPLOY_PATH`. +Only the backend deploy updates the compose files on the server. The MCP server deploy uses the files already there, so republishing an older MCP release cannot bring back older compose files. **Changes to either compose file reach production with the next backend release**, including changes that only affect the MCP server service; release the backend before an MCP release that depends on them. The backend deploy only recreates `bitfinance-api`; changes to other services apply when those containers are next recreated. + ### Frontend 1. Builds the app with `.env.production.example`, writes `dist/version.json` (version, commit, tag, build time), and keeps the archive as a workflow artifact for 90 days. @@ -126,9 +129,8 @@ GitHub secrets used by the publish workflows: Server-side requirements: -- `DEPLOY_PATH` contains `docker-compose.yml`, `docker-compose.prod.yml` and `.env`. Use `apps/backend/.env.prod.example` as the template for `.env`. +- `DEPLOY_PATH` contains `.env`, created from the `apps/backend/.env.prod.example` template. The backend deploy writes the compose files; `.env` is the only file kept by hand. Add any variable a release needs to `.env` before releasing it, or the deploy stops at the compose check. - The server's `.env` sets the S3 storage variables (`S3_BUCKET_NAME`, `S3_REGION`, `AWS_ACCESS_KEY_ID`, `AWS_SECRET_ACCESS_KEY`). See "File storage" in `apps/backend/README.md`. -- The deploy does not copy the compose files. After changing `docker-compose.yml` or `docker-compose.prod.yml`, copy them to `DEPLOY_PATH` yourself. - The external Postgres volume (`POSTGRES_DATA_VOLUME`, default `bitfinance_postgres-data`) exists. See `apps/backend/README.md`. - The SSH user can run `docker compose` and can write to `/var/www/bitfinance` without `sudo`.