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
5 changes: 0 additions & 5 deletions .github/workflows/backend-docker-publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -242,9 +242,6 @@ 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" \
Expand Down Expand Up @@ -340,8 +337,6 @@ jobs:
fi
echo "Backend is running $RUNNING_IMAGE"

# Record the deployed version in .env so compose commands run by hand
# on the server keep this image instead of an older tag.
if grep -q '^IMAGE_TAG=' .env; then
sed -i "s/^IMAGE_TAG=.*/IMAGE_TAG=$IMAGE_TAG/" .env
else
Expand Down
64 changes: 53 additions & 11 deletions .github/workflows/mcp-docker-publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,14 @@ jobs:
environment: production

steps:
- name: Checkout compose file
uses: actions/checkout@v6
with:
ref: mcp-server/v${{ needs.validate.outputs.version }}
sparse-checkout: |
apps/mcp-server/docker-compose.yml
sparse-checkout-cone-mode: false

- name: Connect to Tailscale
uses: tailscale/github-action@v4
with:
Expand Down Expand Up @@ -219,21 +227,57 @@ jobs:
printf '%s' "$SSH_PRIVATE_KEY" > "$KEY_FILE"
chmod 600 "$KEY_FILE"

ssh -i "$KEY_FILE" \
-p "$SSH_PORT" \
-o BatchMode=yes \
-o IdentitiesOnly=yes \
-o StrictHostKeyChecking=yes \
SSH_OPTS=(
-o BatchMode=yes
-o IdentitiesOnly=yes
-o StrictHostKeyChecking=yes
)

MCP_DEPLOY_PATH="$DEPLOY_PATH/mcp-server"

echo "Uploading compose file from mcp-server/v$MCP_IMAGE_TAG..."
scp -i "$KEY_FILE" -P "$SSH_PORT" "${SSH_OPTS[@]}" \
apps/mcp-server/docker-compose.yml \
"$SSH_USERNAME@$TARGET_HOST:$MCP_DEPLOY_PATH/docker-compose.yml.new"

printf -v REMOTE_MCP_IMAGE_TAG '%q' "$MCP_IMAGE_TAG"
printf -v REMOTE_MCP_DEPLOY_PATH '%q' "$MCP_DEPLOY_PATH"

ssh -i "$KEY_FILE" -p "$SSH_PORT" "${SSH_OPTS[@]}" \
"$SSH_USERNAME@$TARGET_HOST" \
"MCP_IMAGE_TAG='$MCP_IMAGE_TAG' DEPLOY_PATH='$DEPLOY_PATH' bash -s" <<'REMOTE'
"MCP_IMAGE_TAG=$REMOTE_MCP_IMAGE_TAG MCP_DEPLOY_PATH=$REMOTE_MCP_DEPLOY_PATH bash -s" <<'REMOTE'
set -euo pipefail

COMPOSE_FILES=(-f docker-compose.yml -f docker-compose.prod.yml)
cd "$DEPLOY_PATH"
COMPOSE_FILES=(-f docker-compose.yml)
cd "$MCP_DEPLOY_PATH"

for file in docker-compose.yml.new .env; do
if [ ! -f "$file" ]; then
echo "::error::Required deploy file is missing: $MCP_DEPLOY_PATH/$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 file against .env..."
if ! CHECK_OUTPUT="$(docker compose -f docker-compose.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
echo "::error::The release's compose file does not work with the server's .env. The current compose file was left in place."
exit 1
fi

if [ -f docker-compose.yml ]; then
cp -p docker-compose.yml docker-compose.yml.prev
fi
mv docker-compose.yml.new docker-compose.yml
echo "Compose file updated from the release (previous version kept as docker-compose.yml.prev)"

echo "Pulling MCP server image tag: $MCP_IMAGE_TAG"
docker compose "${COMPOSE_FILES[@]}" pull bitfinance-mcp-server
docker compose "${COMPOSE_FILES[@]}" up -d --no-deps --no-build --wait --wait-timeout 120 bitfinance-mcp-server
docker compose "${COMPOSE_FILES[@]}" up -d --no-build --wait --wait-timeout 120 bitfinance-mcp-server

EXPECTED_IMAGE="gustmrg/bitfinance-mcp-server:$MCP_IMAGE_TAG"
RUNNING_IMAGE="$(docker inspect --format '{{.Config.Image}}' "$(docker compose "${COMPOSE_FILES[@]}" ps -q bitfinance-mcp-server)")"
Expand All @@ -243,8 +287,6 @@ jobs:
fi
echo "MCP server is running $RUNNING_IMAGE"

# Record the deployed version in .env so compose commands run by hand
# on the server keep this image instead of an older tag.
if grep -q '^MCP_IMAGE_TAG=' .env; then
sed -i "s/^MCP_IMAGE_TAG=.*/MCP_IMAGE_TAG=$MCP_IMAGE_TAG/" .env
else
Expand Down
13 changes: 0 additions & 13 deletions apps/backend/.env.prod.example
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
# Image Tag (e.g., 1.7.0, latest)
IMAGE_TAG=latest
MCP_IMAGE_TAG=latest

# Database Configuration (REQUIRED - no defaults in production)
DB_USER=
Expand All @@ -25,8 +24,6 @@ AZURE_CLIENT_SECRET=
AZURE_TENANT_ID=

# File storage (Amazon S3, REQUIRED in production)
# Credentials of the IAM user that can read, write and delete objects in the bucket.
# Leave Storage__ServiceUrl unset: it is only for S3-compatible servers such as local MinIO.
S3_BUCKET_NAME=
S3_REGION=us-east-1
AWS_ACCESS_KEY_ID=
Expand All @@ -44,13 +41,3 @@ RESEND_API_TOKEN=
RESEND_WEBHOOK_SECRET=
NOTIFICATIONS_FROM_ADDRESS=
NOTIFICATIONS_FRONTEND_BASE_URL=

# MCP Server Configuration
MCP_API_BASE_URL=http://bitfinance-api:8080
MCP_AGENT_EMAIL=
MCP_AGENT_PASSWORD=
MCP_DEFAULT_ORGANIZATION_ID=
MCP_API_VERSION=1
MCP_BIND_ADDRESS=
MCP_PORT=8090
MCP_BEARER_TOKEN=
24 changes: 0 additions & 24 deletions apps/backend/docker-compose.prod.yml
Original file line number Diff line number Diff line change
Expand Up @@ -37,30 +37,6 @@ services:
max-size: "10m"
max-file: "3"

bitfinance-mcp-server:
image: gustmrg/bitfinance-mcp-server:${MCP_IMAGE_TAG:-latest}
container_name: bitfinance-mcp-server
restart: always
ports:
- "${MCP_BIND_ADDRESS}:${MCP_PORT:-8090}:8090"
environment:
- ASPNETCORE_URLS=http://+:8090
- BITFINANCE_API_BASE_URL=${MCP_API_BASE_URL:-http://bitfinance-api:8080}
- BITFINANCE_AGENT_EMAIL=${MCP_AGENT_EMAIL}
- BITFINANCE_AGENT_PASSWORD=${MCP_AGENT_PASSWORD}
- BITFINANCE_DEFAULT_ORGANIZATION_ID=${MCP_DEFAULT_ORGANIZATION_ID:-}
- BITFINANCE_API_VERSION=${MCP_API_VERSION:-1}
- BITFINANCE_MCP_BEARER_TOKEN=${MCP_BEARER_TOKEN}
depends_on:
- bitfinance-api
networks:
- bitfinance-network
logging:
driver: "json-file"
options:
max-size: "10m"
max-file: "3"

bitfinance-db:
restart: always
ports:
Expand Down
1 change: 1 addition & 0 deletions apps/backend/docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ services:

networks:
bitfinance-network:
name: bitfinance_bitfinance-network
driver: bridge

volumes:
Expand Down
14 changes: 14 additions & 0 deletions apps/mcp-server/.env.prod.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
# Image Tag (e.g., 0.5.1, latest)
MCP_IMAGE_TAG=latest

# BitFinance API
MCP_API_BASE_URL=http://bitfinance-api:8080
MCP_API_VERSION=1
MCP_AGENT_EMAIL=
MCP_AGENT_PASSWORD=
MCP_DEFAULT_ORGANIZATION_ID=

# MCP endpoint
MCP_BIND_ADDRESS=
MCP_PORT=8090
MCP_BEARER_TOKEN=
2 changes: 2 additions & 0 deletions apps/mcp-server/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,8 @@ Example generic remote MCP configuration:

For production, bind the Docker port to the VPS Tailscale address and firewall port `8090` so only the Raspberry Pi tailnet IP can reach it. Do not publish the BitFinance API publicly for Hermes; keep `BITFINANCE_API_BASE_URL=http://bitfinance-api:8080` inside the MCP container.

`docker-compose.yml` in this directory runs the server in production as its own compose project, separate from the backend's. It joins the backend's Docker network (`bitfinance_bitfinance-network`) to reach the API, so start the backend stack first. Copy `.env.prod.example` to `.env` next to it and fill in the values, then run `docker compose up --no-build -d`. See `docs/deployment.md` for how releases deploy it.

## Tools

- `bitfinance_list_organizations`
Expand Down
29 changes: 29 additions & 0 deletions apps/mcp-server/docker-compose.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
name: bitfinance-mcp

services:
bitfinance-mcp-server:
image: gustmrg/bitfinance-mcp-server:${MCP_IMAGE_TAG:-latest}
container_name: bitfinance-mcp-server
restart: always
ports:
- "${MCP_BIND_ADDRESS}:${MCP_PORT:-8090}:8090"
environment:
- ASPNETCORE_URLS=http://+:8090
- BITFINANCE_API_BASE_URL=${MCP_API_BASE_URL:-http://bitfinance-api:8080}
- BITFINANCE_AGENT_EMAIL=${MCP_AGENT_EMAIL}
- BITFINANCE_AGENT_PASSWORD=${MCP_AGENT_PASSWORD}
- BITFINANCE_DEFAULT_ORGANIZATION_ID=${MCP_DEFAULT_ORGANIZATION_ID:-}
- BITFINANCE_API_VERSION=${MCP_API_VERSION:-1}
- BITFINANCE_MCP_BEARER_TOKEN=${MCP_BEARER_TOKEN}
networks:
- bitfinance-network
logging:
driver: "json-file"
options:
max-size: "10m"
max-file: "3"

networks:
bitfinance-network:
external: true
name: bitfinance_bitfinance-network
24 changes: 17 additions & 7 deletions docs/deployment.md
Original file line number Diff line number Diff line change
Expand Up @@ -86,14 +86,24 @@ The deploy prints the service status and recent logs, including when it fails.
### MCP server

1. Builds `gustmrg/bitfinance-mcp-server` for `linux/amd64` and `linux/arm64` and pushes it to Docker Hub as `<version>` and `latest`.
2. On the server, in `DEPLOY_PATH`, with `MCP_IMAGE_TAG=<version>`:
2. Uploads `apps/mcp-server/docker-compose.yml` from the release tag to `DEPLOY_PATH/mcp-server`, checks it with `docker compose config` against that directory's `.env`, and only then puts it in place of the current file, which is kept as `docker-compose.yml.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/mcp-server`, with `MCP_IMAGE_TAG=<version>`:
- pulls the new `bitfinance-mcp-server` image;
- recreates the `bitfinance-mcp-server` container from it. `--no-deps` leaves the API, database and cache untouched.
- records the version as `MCP_IMAGE_TAG` in the server's `.env`.
- recreates the `bitfinance-mcp-server` container from it;
- records the version as `MCP_IMAGE_TAG` in that directory's `.env`.

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`.
### Backend and MCP server compose projects

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.
The backend and the MCP server are separate compose projects, so each one deploys, and rolls back, without the other:

| App | Compose files | Directory on the server | Project |
|---|---|---|---|
| backend | `apps/backend/docker-compose.yml`, `docker-compose.prod.yml` | `DEPLOY_PATH` | `bitfinance` |
| mcp-server | `apps/mcp-server/docker-compose.yml` | `DEPLOY_PATH/mcp-server` | `bitfinance-mcp` |

Each deploy only replaces its own compose files, taken from its own release tag, and each directory has its own `.env`. A change to a compose file reaches production with the next release of that app.

The MCP server reaches the API at `http://bitfinance-api:8080` over the backend's Docker network, `bitfinance_bitfinance-network`, which it joins as an external network. The backend stack must therefore be running, and the network keeps that fixed name. The backend deploy only recreates `bitfinance-api`; changes to the database or cache services apply when those containers are next recreated.

### Frontend

Expand Down Expand Up @@ -125,11 +135,11 @@ GitHub secrets used by the publish workflows:
| `SSH_HOST` | All | Fallback server address |
| `SSH_USERNAME`, `SSH_KEY` | All | SSH login |
| `SSH_PORT` | All | Optional, defaults to `22` |
| `DEPLOY_PATH` | Backend, MCP server | Directory with the compose files and `.env` on the server |
| `DEPLOY_PATH` | Backend, MCP server | Directory with the backend's compose files and `.env` on the server; the MCP server uses its `mcp-server` subdirectory |

Server-side requirements:

- `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.
- `DEPLOY_PATH` contains `.env`, created from `apps/backend/.env.prod.example`, and `DEPLOY_PATH/mcp-server` contains `.env`, created from `apps/mcp-server/.env.prod.example`. The deploys write the compose files; the `.env` files are the only files kept by hand. Add any variable a release needs to its app's `.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 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