A deploy puts a new version of your app live on its server. moin numbers them per app: v1, v2, v3.
Commands that work on an app take its name as the last word, like moin redeploy <app-name>. Leave it out and moin uses the app of the folder you are in: the app in x-moin, or else the folder’s name.
Deploy from a repo
Sends the code in your current folder to the server, which builds and starts it.
moin deployThe web and MCP cannot reach the files on your computer. They deploy from GitHub or an image instead. In the API, upload first, then start the deploy.
moin deploy # the app of this folder
moin deploy <app-name> # a named app
moin deploy --server <server-name> --dir ./site <app-name> # a new app on one of several servers, from another folder
moin packs the files git knows, including changes you have not committed and new files that are not ignored. Outside a git repo it packs everything but .git. The archive goes straight into your bucket. The agent downloads it from there, deletes it, and builds and starts it with Docker Compose on the app’s server. moin never stores your code.
What the repo brings decides how moin builds it:
| The repo has | moin does |
|---|---|
compose.yml |
uses it |
only a Dockerfile |
runs it as service web, mounts ./data at /data |
| a Go module | also writes the Dockerfile: it builds ., ./cmd/server or the only package under cmd/, and sets PORT and DATA_DIR=/data |
| none of these | builds it with Railpack: Node, Bun, Python, Ruby, PHP and more |
Your app must listen on the port moin gives it in PORT, or on the port in your compose file or Dockerfile. Config in the repo changes the port, the health check and more.
No Dockerfile
No Dockerfile? No problem. moin reads your repo and builds it with Railpack, the open source builder Railway made: Node, Bun, Python, Ruby, PHP, Deno, Rust, Elixir, Java, .NET, static sites and more.
- It finds the language and the version: from
package.json,requirements.txt,pyproject.toml,Gemfile,composer.jsonand the like. - It finds the start command: the
startscript, aProcfile,main.py. Set your own with the env varRAILPACK_START_CMD. - Your app listens on
PORT: moin sets it to 8080 and sends traffic there, like Railway. - Your env vars reach the build, as secrets that never land in the image or in a log. A build that needs
NEXT_PUBLIC_API_URLgets it. - Data goes to
/data, like any app moin builds:DATA_DIR=/datais set.
The deploy says what it found:
→ Railpack: Node 24.22.1, start: npm run start
The app page, moin apps <app-name>, the TUI, the API and get_app show it too. If Railpack cannot tell how to build or start the repo, the deploy fails with its reason; add a Dockerfile or a compose.yml and deploy again. The agent downloads Railpack once, checked against its published checksum, and the build runs on your server with Docker’s own BuildKit.
See your apps
Lists every app with its server and its live deploy.
Apps
Create an app
Makes an empty app on a server, ready for its first deploy. The first moin deploy in a repo does this by itself.
Apps, Add app
What a deploy does
- Downloads the code from your bucket, or the commit from GitHub, checks it and keeps it with the release.
- Reads the compose file with
docker compose config, together with the x-moin block. - Tags the built images per deploy, sets the env vars and
restart: unless-stopped. - Builds, starts with
docker compose up --waitand checks that the main service still runs 5 seconds later. - Calls the health path, if x-moin sets one, until it answers below 500.
- Reports the SQLite databases it finds on the app’s volumes and the Postgres services in its compose file, for the data browser and the backups.
If the start fails, the previous release starts again and the deploy is marked failed. Its log ends with the last lines of the service that stopped.
The server keeps the releases of the last 5 builds (of code or of an image) with their images, and every restart, start, domain change and rollback since the oldest of them. Roll back can go to any of those.
Your data survives every deploy
A volume is a folder on the server that your app’s container can see, such as ./data on the server shown as /data in the container. A folder your app mounts but the repo does not bring, like ./data, moves to /var/lib/moin/apps/<app>/volumes/ and stays there across deploys. Named volumes are kept as they are. Files the repo brings, like a config file, belong to the release.
Your app keeps its database in a folder other than /data? Mount that folder in compose.yml, for example ./data:/app/data. moin keeps it and backs up the SQLite files in it.
Follow a deploy
Shows the log of a deploy while it runs, step by step.
the app page, Deploy log
moin deploy prints the log as it goes. An agent over MCP calls get_deploy again until the deploy is done. The web page follows every new deploy by itself, also one started from the CLI.
See past deploys
Lists the deploys of an app, newest first, with their status.
the app page, Deploys
Read the log of one deploy
Shows the full log of an earlier deploy. Every deploy keeps its log, a failed one too, so you can read later what went wrong. A failed deploy’s log ends with the last lines of the service that stopped: start there.
the app page, click the deploy under Deploys
Redeploy
Builds the newest code again from the copy the server kept, with the env vars as they are now. Nothing is uploaded.
the app page, Redeploy
After changing env vars, a restart is enough and faster. An app deployed from an image pulls and runs the newest image again instead.
Restart
Starts the live release again with the env vars, domains and password as they are now. Nothing is built. Use it after changing env vars.
If the app’s images are gone from the server, for example after docker image prune, the restart builds them again from the source the release kept. If that source is gone too, it says so: deploy again.
the app page, Restart
Stop
Takes the app offline. It stays off, also after the server reboots. Its data and settings stay.
the app page, Stop
moin stop asks first; --yes skips the question. Changes to the domains or the password of a stopped app wait until it starts.
Start
Runs a stopped app again, like a restart. A deploy starts it too. An app that runs already stays as it is: moin says it is already running.
the app page, Start
Roll back
Starts an earlier successful deploy again, with the env vars it had. Use it when a new version is broken.
the app page, Roll back on a deploy
A rollback is a new deploy, for example v4 with the note rollback to v3. Without a number (moin rollback), it picks the successful deploy before the live one. In the TUI, pick a deploy with ↑ ↓ on the deploys tab before r; P takes the one before the live one. In the API, send the deploy as {"number": 3}. The web, the CLI and the TUI ask first; in a script, add --yes.
Remove an app
Stops the app and removes its containers and images. Its volumes and data stay on the server, and its backups stay in your bucket until you delete them: see How long things stay.
the app page, Remove app
You cannot undo it, so moin asks you to type the app’s name first. moin apps rm <app-name> --yes skips that in scripts. An agent passes the name as confirm.
Order your apps
Apps show in the order you give them, the same for everyone in the workspace and everywhere: the apps page and the sidebar, the CLI, the TUI, the API and MCP. A new app goes to the end.
Apps, drag an app to its place
With a mouse you drag the whole app; on a phone you drag the grip at its left, so the page still scrolls. On the keyboard, Alt and the up or down arrow move the app in focus. The API and MCP take every app once, by name or ID, in the new order: {"order": ["blog", "shop"]}. The activity records each new order once.
List and Grid above the apps switch how the page shows them: one row each, or cards side by side. moin remembers your choice for you on every device; the rest of the team keeps theirs.
Push to deploy with GitHub Actions
This workflow deploys every push to main. The GitHub guide shows a way without CI, and Build in CI builds the image in CI.
It needs a bucket and an API token with Read and write access. Create the token and save it in your GitHub repo under Settings, Secrets and variables, Actions, as MOIN_TOKEN.
name: deploy
on:
push:
branches: [main]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
- run: go install github.com/axadrn/moin/cmd/moin@latest
- run: moin deploy
env:
MOIN_URL: https://moin.run
MOIN_TOKEN: ${{ secrets.MOIN_TOKEN }}