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
3 changes: 3 additions & 0 deletions .env
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@ USE_PROXY_AUTH=0
PROXY_AUTH_USER=
PROXY_AUTH_PASS=
PUBLIC_ADDRESS=https://localhost:8443
ERAMBA_MCP_IMAGE_TAG=3.x
OAUTH2_INTROSPECTION_CLIENT_ID=eramba-mcp-introspect
OAUTH2_INTROSPECTION_CLIENT_SECRET=Replace-this-with-a-unique-secret
DOCKER_DEPLOYMENT=1
LDAPTLS_REQCERT=never
TRIGGER_RUNNER_SHARED_SECRET=c86b1a9edd5fdad7f85c95775f42246d761c5486b9059b7db23efc69410b9eaf
47 changes: 39 additions & 8 deletions .github/workflows/Docker.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,10 @@ on:
branches:
- '*'

permissions:
contents: read
packages: read

jobs:
simple_install:
name: Simple Installation [${{ matrix.build-type }}]
Expand All @@ -19,20 +23,22 @@ jobs:
steps:
- name: Check out the repo
uses: actions/checkout@v3
#
# - name: Log in to GitHub Container Registry
# uses: docker/login-action@v2
# with:
# registry: ghcr.io
# username: ${{ github.actor }}
# password: ${{ secrets.TOKEN_INCREASED_PERMISSIONS }}

- name: Check MCP simple-install contract
run: npx --yes bats@1.12.0 tests/mcp-simple-install.bats

- name: Log in to GitHub Container Registry
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Run Simple Install
run: |
docker compose -f docker-compose.simple-install.yml down 2>&1
docker system prune --force --volumes --all

docker compose -f docker-compose.simple-install.yml up -d
ERAMBA_MCP_DEV_BEARER_TOKEN=ci-mcp-smoke-token docker compose -f docker-compose.simple-install.yml up -d

- name: Wait for apache to start
timeout-minutes: 5
Expand All @@ -46,6 +52,31 @@ jobs:
exit 1
fi

- name: MCP protocol check
timeout-minutes: 5
run: |
until curl --fail --silent --insecure https://localhost:8443/openapi.json >/dev/null; do
printf 'Waiting for MCP...\n'
sleep 2
done

curl --fail --silent --insecure https://localhost:8443/.well-known/oauth-protected-resource/mcp \
| jq -e '.resource == "https://localhost:8443/mcp"'

curl --fail --silent --insecure https://localhost:8443/mcp \
-H 'Authorization: Bearer ci-mcp-smoke-token' \
-H 'Content-Type: application/json' \
-H 'Accept: application/json, text/event-stream' \
--data '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-03-26","capabilities":{},"clientInfo":{"name":"docker-ci","version":"1.0"}}}' \
| jq -e '.result.protocolVersion == "2025-03-26"'

curl --fail --silent --insecure https://localhost:8443/mcp \
-H 'Authorization: Bearer ci-mcp-smoke-token' \
-H 'Content-Type: application/json' \
-H 'Accept: application/json, text/event-stream' \
--data '{"jsonrpc":"2.0","id":2,"method":"tools/list","params":{}}' \
| jq -e '.result.tools | length > 0'

- name: CMD Check
run: |
if docker exec -w /var/www/eramba/app/upgrade -u www-data eramba bin/cake current_config validate; then
Expand Down
18 changes: 18 additions & 0 deletions Caddyfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
:443 {
tls /certs/mycert.crt /certs/mycert.key

# Keep Inspector local-only. Proxying the UI and its local bridge through the
# same public origin as Eramba/MCP makes OAuth and app rendering brittle.
@mcp path /mcp /openapi /openapi.json /.well-known/oauth-protected-resource /.well-known/oauth-protected-resource/mcp /.well-known/oauth-authorization-server /.well-known/openid-configuration
handle @mcp {
reverse_proxy {$MCP_UPSTREAM:mcp_server:3000}
}

handle {
reverse_proxy {$ERAMBA_UPSTREAM:https://eramba:443} {
transport http {
tls_insecure_skip_verify
}
}
}
}
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
Welcome to eramba's official Github account, for Docker installs please review our website Learning Platform ([eramba.org](https://www.eramba.org/learning/courses/12/episodes/274)) under Docker Install.

The bundled files in `apache/ssl/` are a branded local development certificate intended only for local or simple demo installs. It is signed by a local development CA and will only be trusted on machines where that CA has been installed. Replace it with your own CA-issued certificate and private key for any real deployment.

Eramba and its MCP endpoint share the `PUBLIC_ADDRESS` origin. With the default configuration the application is available at `https://localhost:8443` and MCP at `https://localhost:8443/mcp`; issuer, resource, metadata, and OpenAPI URLs are derived automatically. Do not add separate public MCP URL variables.

Before starting a real deployment, replace `OAUTH2_INTROSPECTION_CLIENT_SECRET` in `.env` with a unique random secret. `ERAMBA_MCP_IMAGE_TAG` defaults to the supported `3.x` MCP image stream and can be pinned when required.
61 changes: 59 additions & 2 deletions docker-compose.simple-install.yml
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,6 @@ services:
container_name: eramba
image: ghcr.io/eramba/eramba:latest
restart: always
ports:
- 8443:443
volumes:
- data:/var/www/eramba/app/upgrade/data
- app:/var/www/eramba
Expand Down Expand Up @@ -55,6 +53,13 @@ services:
TRIGGER_RUNNER_SHARED_SECRET: ${TRIGGER_RUNNER_SHARED_SECRET}
DOCKER_DEPLOYMENT: ${DOCKER_DEPLOYMENT}
LDAPTLS_REQCERT: ${LDAPTLS_REQCERT}
OAUTH2_ENABLED: ${OAUTH2_ENABLED:-1}
OAUTH2_SCOPE: ${OAUTH2_SCOPE:-eramba.full_access}
OAUTH2_AUTH_CODE_TTL: ${OAUTH2_AUTH_CODE_TTL:-120}
OAUTH2_ACCESS_TOKEN_TTL: ${OAUTH2_ACCESS_TOKEN_TTL:-900}
OAUTH2_REFRESH_TOKEN_TTL: ${OAUTH2_REFRESH_TOKEN_TTL:-2592000}
OAUTH2_INTROSPECTION_CLIENT_ID: ${OAUTH2_INTROSPECTION_CLIENT_ID:?Set OAUTH2_INTROSPECTION_CLIENT_ID in .env}
OAUTH2_INTROSPECTION_CLIENT_SECRET: ${OAUTH2_INTROSPECTION_CLIENT_SECRET:?Set OAUTH2_INTROSPECTION_CLIENT_SECRET in .env}
links:
- mysql
- redis
Expand Down Expand Up @@ -92,6 +97,13 @@ services:
TRIGGER_RUNNER_SHARED_SECRET: ${TRIGGER_RUNNER_SHARED_SECRET}
DOCKER_DEPLOYMENT: ${DOCKER_DEPLOYMENT}
LDAPTLS_REQCERT: ${LDAPTLS_REQCERT}
OAUTH2_ENABLED: ${OAUTH2_ENABLED:-1}
OAUTH2_SCOPE: ${OAUTH2_SCOPE:-eramba.full_access}
OAUTH2_AUTH_CODE_TTL: ${OAUTH2_AUTH_CODE_TTL:-120}
OAUTH2_ACCESS_TOKEN_TTL: ${OAUTH2_ACCESS_TOKEN_TTL:-900}
OAUTH2_REFRESH_TOKEN_TTL: ${OAUTH2_REFRESH_TOKEN_TTL:-2592000}
OAUTH2_INTROSPECTION_CLIENT_ID: ${OAUTH2_INTROSPECTION_CLIENT_ID:?Set OAUTH2_INTROSPECTION_CLIENT_ID in .env}
OAUTH2_INTROSPECTION_CLIENT_SECRET: ${OAUTH2_INTROSPECTION_CLIENT_SECRET:?Set OAUTH2_INTROSPECTION_CLIENT_SECRET in .env}
links:
- mysql
- redis
Expand Down Expand Up @@ -124,6 +136,51 @@ services:
timeout: 5s
retries: 3
start_period: 10s
mcp_server:
image: ghcr.io/eramba/eramba-mcp-server:${ERAMBA_MCP_IMAGE_TAG:-3.x}
container_name: mcp_server
restart: always
depends_on:
- eramba
environment:
MCP_TRANSPORT: http
PORT: 3000
PUBLIC_ADDRESS: ${PUBLIC_ADDRESS}
ERAMBA_BASE_URL: https://eramba:443
ERAMBA_OAUTH_INTROSPECTION_URL: https://eramba:443/oauth2/introspect
ERAMBA_OAUTH_INTROSPECTION_CLIENT_ID: ${OAUTH2_INTROSPECTION_CLIENT_ID:?Set OAUTH2_INTROSPECTION_CLIENT_ID in .env}
ERAMBA_OAUTH_INTROSPECTION_CLIENT_SECRET: ${OAUTH2_INTROSPECTION_CLIENT_SECRET:?Set OAUTH2_INTROSPECTION_CLIENT_SECRET in .env}
MCP_REQUIRED_SCOPE: ${OAUTH2_SCOPE:-eramba.full_access}
ERAMBA_MCP_DEV_BEARER_TOKEN: ${ERAMBA_MCP_DEV_BEARER_TOKEN:-}
ERAMBA_INSECURE_TLS: 1
ERAMBA_REQUEST_TIMEOUT_MS: 15000
networks:
- app_internal
- host_access
healthcheck:
test: ["CMD-SHELL", "node -e \"fetch('http://127.0.0.1:3000/openapi.json').then(r=>process.exit(r.ok?0:1)).catch(()=>process.exit(1))\""]
interval: 30s
timeout: 5s
retries: 3
start_period: 10s
public_proxy:
container_name: public_proxy
image: caddy:2.8-alpine
ports:
- 8443:443
volumes:
- ./Caddyfile:/etc/caddy/Caddyfile:ro
- ./apache/ssl/mycert.crt:/certs/mycert.crt:ro
- ./apache/ssl/mycert.key:/certs/mycert.key:ro
environment:
MCP_UPSTREAM: mcp_server:3000
ERAMBA_UPSTREAM: https://eramba:443
depends_on:
- eramba
- mcp_server
networks:
- app_internal
- host_access
volumes:
app:
data:
Expand Down
42 changes: 42 additions & 0 deletions tests/mcp-simple-install.bats
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
#!/usr/bin/env bats

setup() {
REPOSITORY_ROOT="$(cd "${BATS_TEST_DIRNAME}/.." && pwd)"

run env -i PATH="$PATH" HOME="$HOME" docker compose \
--project-directory "$REPOSITORY_ROOT" \
--env-file "$REPOSITORY_ROOT/.env" \
-f "$REPOSITORY_ROOT/docker-compose.simple-install.yml" \
config --format json

[ "$status" -eq 0 ]
COMPOSE_CONFIG="$output"
}

@test "simple install keeps release app images and uses the MCP 3.x image" {
[ "$(jq -r '.services.eramba.image' <<<"$COMPOSE_CONFIG")" = "ghcr.io/eramba/eramba:latest" ]
[ "$(jq -r '.services.cron.image' <<<"$COMPOSE_CONFIG")" = "ghcr.io/eramba/eramba:latest" ]
[ "$(jq -r '.services.triggers_caddy.image' <<<"$COMPOSE_CONFIG")" = "ghcr.io/eramba/eramba-triggers:latest" ]
[ "$(jq -r '.services.mcp_server.image' <<<"$COMPOSE_CONFIG")" = "ghcr.io/eramba/eramba-mcp-server:3.x" ]
}

@test "Caddy is the only published application edge" {
[ "$(jq -r '(.services.eramba.ports // []) | length' <<<"$COMPOSE_CONFIG")" -eq 0 ]
[ "$(jq -r '(.services.mcp_server.ports // []) | length' <<<"$COMPOSE_CONFIG")" -eq 0 ]
[ "$(jq -r '.services.public_proxy.ports | length' <<<"$COMPOSE_CONFIG")" -eq 1 ]
[ "$(jq -r '.services.public_proxy.ports[0] | "\(.published):\(.target)"' <<<"$COMPOSE_CONFIG")" = "8443:443" ]
}

@test "MCP derives public metadata from PUBLIC_ADDRESS and introspects internally" {
[ "$(jq -r '.services.mcp_server.environment.PUBLIC_ADDRESS' <<<"$COMPOSE_CONFIG")" = "https://localhost:8443" ]
[ "$(jq -r '.services.mcp_server.environment.ERAMBA_OAUTH_INTROSPECTION_URL' <<<"$COMPOSE_CONFIG")" = "https://eramba:443/oauth2/introspect" ]

for override in MCP_PUBLIC_URL MCP_RESOURCE ERAMBA_OAUTH_ISSUER OAUTH2_ISSUER OAUTH2_RESOURCE; do
[ "$(jq -r --arg name "$override" '.services.mcp_server.environment | has($name)' <<<"$COMPOSE_CONFIG")" = "false" ]
done
}

@test "Caddy receives the bundled TLS certificate and key" {
[ "$(jq -r '[.services.public_proxy.volumes[].target] | index("/certs/mycert.crt") != null' <<<"$COMPOSE_CONFIG")" = "true" ]
[ "$(jq -r '[.services.public_proxy.volumes[].target] | index("/certs/mycert.key") != null' <<<"$COMPOSE_CONFIG")" = "true" ]
}
Loading