Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 39 additions & 1 deletion .github/workflows/backend-docker-publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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"

Expand Down Expand Up @@ -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

Expand Down
8 changes: 5 additions & 3 deletions docs/deployment.md
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,8 @@ All publish workflows build from the release tag (`<app>/v<version>`), first che
### Backend

1. Builds `gustmrg/bitfinance-backend` for `linux/amd64` and `linux/arm64` and pushes it to Docker Hub as `<version>` 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=<version>`;
- apply database migrations with `docker compose run --rm --no-deps bitfinance-api --migrate`;
- recreate the `bitfinance-api` container from the new image.
Expand All @@ -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.
Expand Down Expand Up @@ -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`.

Expand Down
Loading