By default your server builds the app, as in Deploy. You can also build the image in CI, push it to a registry and let moin deploy it. The server then only pulls and starts it.
| Server builds | CI builds | |
|---|---|---|
| Who builds | your server, from the code moin deploy uploads |
GitHub Actions or any CI |
| Start it | moin deploy |
the deploy hook, or moin deploy --image (after one moin deploy of the repo, whose compose file it reuses) |
| Good for | small apps, no registry needed | heavy builds, build caches, many servers |
An image deploy keeps the compose file of the running release and only swaps the image of the main service. So deploy the repo once with moin deploy; after that CI can ship images. Health check, automatic rollback, moin rollback and env vars work the same.
1. Save a registry login
Lets your servers pull private images. A registry is where CI stores the images it builds, such as GitHub Container Registry. Public images need no login.
Settings, Tokens tab, Registries, Save login
For GitHub Container Registry, create a token with read:packages. The CLI asks for it on stdin. The token is stored encrypted. The agent gets it only for the pull, in a throwaway Docker config; nothing stays on the server.
See the registry logins
Lists the saved logins, without their passwords.
Settings, Tokens tab, Registries
Remove a registry login
Deletes a saved login. Servers can then no longer pull private images from that registry.
Settings, Tokens tab, Registries, Remove on the login
2. Create a deploy hook
Gives you a secret URL that deploys an image when CI calls it.
the app page, Deploy from CI, Show, Deploy hook, New deploy hook
moin shows the URL once. Store it as the CI secret MOIN_DEPLOY_HOOK. A new hook makes the old one stop working, so moin asks first when the app has a hook already; the first one needs no question. In a script, add --yes.
Turn the deploy hook off
Makes the hook URL stop working, so CI can no longer deploy. Do it when you stop using CI or think the URL leaked. A new hook can be made again any time.
the app page, Deploy from CI, Deploy hook, Turn off
It works also when the plan allows no changes. get_app and GET /api/v1/apps/{app} say in hook whether the app has one.
3. The workflow
name: deploy
on:
push:
branches: [main]
permissions:
contents: read
packages: write
jobs:
deploy:
runs-on: ubuntu-24.04-arm
steps:
- uses: actions/checkout@v4
- uses: docker/setup-buildx-action@v3
- uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- uses: docker/build-push-action@v6
with:
push: true
platforms: linux/arm64
tags: ghcr.io/${{ github.repository }}:${{ github.sha }}
- run: |
curl -fsS -X POST \
"$MOIN_DEPLOY_HOOK?image=ghcr.io/${{ github.repository }}:${{ github.sha }}"
env:
MOIN_DEPLOY_HOOK: ${{ secrets.MOIN_DEPLOY_HOOK }}
Build for the arch of your server (moin servers shows it). The hook answers 202 with the deploy; follow it in the web UI, the TUI or with moin logs.
Or deploy with the moin action
The moin GitHub Action installs moin from your control plane and runs moin deploy with a personal API token. Without image it puts the repo into your bucket and your server builds it; with image it deploys the image the job pushed.
- uses: axadrn/moin@main
with:
url: https://moin.run
token: ${{ secrets.MOIN_TOKEN }}
image: ghcr.io/${{ github.repository }}:${{ github.sha }}
The step waits until the deploy is live and fails the job when the deploy fails. For deploys without CI, GitHub deploys every push by itself.
Deploy an image by hand
Deploys an image that is already in a registry, without CI.
the app page, Deploy from CI, Show, Deploy an image, Deploy image
In the API, send {"image": "ghcr.io/<owner>/<image>:<tag>"}. A redeploy of an image deploy pulls the same image again.