Repository navigation
Deploy Cloud #198
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: Deploy Cloud | |
| on: | |
| workflow_dispatch: | |
| inputs: | |
| tag: | |
| description: "Docker image tag to deploy (default: latest)" | |
| required: false | |
| default: "latest" | |
| type: string | |
| # One deploy at a time. On 2026-09-25 two runs of the same commit started two | |
| # minutes apart; the second failed in `docker pull` ("unable to lease | |
| # content") while the first was swapping containers. Worse cases are easy to | |
| # picture: a second run recording the first's half-started release as its | |
| # rollback point, or sharing /tmp/anythingmcp-deploy with it. Queue instead. | |
| concurrency: | |
| group: deploy-cloud | |
| cancel-in-progress: false | |
| jobs: | |
| deploy: | |
| name: Deploy to Cloud Droplet | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v6 | |
| - name: Copy config files to server | |
| uses: appleboy/scp-action@v1.0.0 | |
| with: | |
| host: ${{ secrets.CLOUD_HOST }} | |
| username: root | |
| key: ${{ secrets.CLOUD_SSH_KEY }} | |
| source: "docker-compose.cloud.yml,deploy/cloud/Caddyfile,deploy/cloud/release.sh,deploy/cloud/uptime-probe.sh,deploy/cloud/systemd/anythingmcp-probe.service,deploy/cloud/systemd/anythingmcp-probe.timer,deploy/cloud/stuck-users-report.sh,deploy/cloud/systemd/anythingmcp-stuck-report.service,deploy/cloud/systemd/anythingmcp-stuck-report.timer,deploy/cloud/trial-report.sh,deploy/cloud/systemd/anythingmcp-trial-report.service,deploy/cloud/systemd/anythingmcp-trial-report.timer,deploy/motis/Dockerfile,deploy/motis/config.yml,deploy/motis/entrypoint.sh" | |
| target: /tmp/anythingmcp-deploy | |
| overwrite: true | |
| - name: Deploy via SSH | |
| uses: appleboy/ssh-action@v1 | |
| env: | |
| CRON_SECRET: ${{ secrets.ONBOARDING_CRON_SECRET }} | |
| LICENSE_SERVICE_TOKEN: ${{ secrets.LICENSE_SERVICE_TOKEN }} | |
| with: | |
| host: ${{ secrets.CLOUD_HOST }} | |
| username: root | |
| key: ${{ secrets.CLOUD_SSH_KEY }} | |
| envs: CRON_SECRET,LICENSE_SERVICE_TOKEN | |
| # The action's default is 10m. A release can take longer (up to | |
| # 5 min waiting for the new containers, the site checks, up to | |
| # 2 min draining the old ones), and a timeout mid-release would | |
| # leave it half done — the old colour still serving, but the job | |
| # unable to say so. | |
| command_timeout: 30m | |
| script: | | |
| set -e | |
| cd /opt/anythingmcp-cloud | |
| # Pull the requested tag AND refresh :latest, since | |
| # docker-compose.cloud.yml references :latest. Without this | |
| # the local :latest tag can point to an older image. | |
| docker pull helpcodeai/anythingmcp:${{ inputs.tag }} | |
| docker pull helpcodeai/anythingmcp:latest | |
| # docker-compose.cloud.yml and the Caddyfile are NOT copied here: | |
| # release.sh installs them, after saving the ones in use, so a | |
| # failed release can put them back. The MOTIS build context is | |
| # copied below; `docker compose up` builds it if the pinned image | |
| # tag is missing (fresh droplet or a bumped tag), otherwise the | |
| # existing image is reused. | |
| # Uptime probe: a systemd timer on this host, every minute, mailing | |
| # on a change of state. It is here and not only in GitHub Actions | |
| # because the scheduled workflow ran three times in its first ten | |
| # hours — GitHub's cron is best-effort — and the failure mode we | |
| # have is the backend dying while the host stays up, which a timer | |
| # on the host sees within a minute. Reads DOMAIN, SMTP_* and | |
| # UPTIME_ALERT_TO from this directory's .env; with no | |
| # UPTIME_ALERT_TO it only logs to the journal. | |
| install -m 755 /tmp/anythingmcp-deploy/deploy/cloud/uptime-probe.sh ./uptime-probe.sh | |
| install -m 644 /tmp/anythingmcp-deploy/deploy/cloud/systemd/anythingmcp-probe.service /etc/systemd/system/ | |
| install -m 644 /tmp/anythingmcp-deploy/deploy/cloud/systemd/anythingmcp-probe.timer /etc/systemd/system/ | |
| systemctl daemon-reload | |
| systemctl enable --now anythingmcp-probe.timer | |
| systemctl is-active anythingmcp-probe.timer | |
| # Weekly stuck-users report, Monday 07:00 Europe/Berlin: orgs whose | |
| # tool calls have never succeeded, connectors failing for everyone, | |
| # signups that never got going. Read-only on the database. Mails | |
| # REPORT_TO (else UPTIME_ALERT_TO) through this directory's SMTP_*; | |
| # with neither set it writes to the journal. The timer does not fire | |
| # on install, only on the next Monday. | |
| install -m 755 /tmp/anythingmcp-deploy/deploy/cloud/stuck-users-report.sh ./stuck-users-report.sh | |
| install -m 644 /tmp/anythingmcp-deploy/deploy/cloud/systemd/anythingmcp-stuck-report.service /etc/systemd/system/ | |
| install -m 644 /tmp/anythingmcp-deploy/deploy/cloud/systemd/anythingmcp-stuck-report.timer /etc/systemd/system/ | |
| systemctl daemon-reload | |
| systemctl enable --now anythingmcp-stuck-report.timer | |
| systemctl is-active anythingmcp-stuck-report.timer | |
| # Daily trial report, 08:05 Europe/Berlin: licences that became paid, | |
| # trials ending in the next 3 days and how far each workspace got, | |
| # trials that ended yesterday. Read-only, same mail route as above. | |
| install -m 755 /tmp/anythingmcp-deploy/deploy/cloud/trial-report.sh ./trial-report.sh | |
| install -m 644 /tmp/anythingmcp-deploy/deploy/cloud/systemd/anythingmcp-trial-report.service /etc/systemd/system/ | |
| install -m 644 /tmp/anythingmcp-deploy/deploy/cloud/systemd/anythingmcp-trial-report.timer /etc/systemd/system/ | |
| systemctl daemon-reload | |
| systemctl enable --now anythingmcp-trial-report.timer | |
| systemctl is-active anythingmcp-trial-report.timer | |
| mkdir -p ./deploy/motis | |
| cp /tmp/anythingmcp-deploy/deploy/motis/* ./deploy/motis/ | |
| rm -rf ./deploy/cloud/db-rest | |
| # Keep CRON_SECRET in the server .env in sync with the repo | |
| # secret so the onboarding-reminders cron can authenticate. | |
| # Upsert (replace-or-append) without disturbing other vars. | |
| touch .env | |
| if grep -q '^CRON_SECRET=' .env; then | |
| sed -i "s#^CRON_SECRET=.*#CRON_SECRET=${CRON_SECRET}#" .env | |
| else | |
| echo "CRON_SECRET=${CRON_SECRET}" >> .env | |
| fi | |
| # Same for the licence service token, which anythingmcp.com checks | |
| # to tell this backend apart from the public when it asks for a | |
| # trial. Must match LICENSE_SERVICE_TOKEN on the website droplet. | |
| if [ -n "${LICENSE_SERVICE_TOKEN}" ]; then | |
| if grep -q '^LICENSE_SERVICE_TOKEN=' .env; then | |
| sed -i "s#^LICENSE_SERVICE_TOKEN=.*#LICENSE_SERVICE_TOKEN=${LICENSE_SERVICE_TOKEN}#" .env | |
| else | |
| echo "LICENSE_SERVICE_TOKEN=${LICENSE_SERVICE_TOKEN}" >> .env | |
| fi | |
| fi | |
| trap 'rm -rf /tmp/anythingmcp-deploy' EXIT | |
| # Blue/green: start the new release next to the one serving, check | |
| # it privately and then publicly, switch Caddy, stop the old one — | |
| # see deploy/cloud/release.sh. A release that fails its checks is | |
| # removed before it serves anything and this job goes red with the | |
| # previous release still serving. The uptime probe is no longer | |
| # silenced for the deploy: a healthy one has no downtime to hide. | |
| # release.sh silences it itself only when it has to fall back to | |
| # stop-then-start (no room on the host for two backends). | |
| bash /tmp/anythingmcp-deploy/deploy/cloud/release.sh | |
| docker image prune -f | |
| # Keep the eight most recent app images and drop the rest. On | |
| # 2026-09-19 the droplet held 35 of them going back four months, | |
| # ~1.3 GB each — the bulk of the 34 GB under /var/lib/containerd, | |
| # growing by one image per deploy. | |
| # | |
| # Safe because these are a local cache, not an archive: every tag | |
| # is still on Docker Hub, so an older rollback target comes back | |
| # with `docker pull helpcodeai/anythingmcp:<tag>`. Eight covers | |
| # roughly six weeks of builds. An image a container is using | |
| # cannot be removed — docker refuses, and the `|| true` keeps the | |
| # deploy green when it does. | |
| docker images helpcodeai/anythingmcp \ | |
| --format '{{.CreatedAt}}\t{{.Repository}}:{{.Tag}}' \ | |
| | sort -r | tail -n +9 | cut -f2 \ | |
| | while read -r img; do | |
| [ "$img" = "helpcodeai/anythingmcp:latest" ] && continue | |
| echo "pruning cached image $img" | |
| docker rmi "$img" >/dev/null 2>&1 || true | |
| done | |
| echo "Deployment complete" |