Skip to content

Under developmentMica is experimental and not ready for production. The API and the content format can still change, so read the release notes before you upgrade.Follow the releases.

Docs/Self-hosting

Operate

Self-hosting

Run one Mica container for editors: repository token, sign-in, who may edit, drafts, the site build, backups, upgrades, and troubleshooting.

20 min read

This page sets up the self-hosted editor for editors who are not developers. It is one Docker container: the image ghcr.io/micacms/mica. If you only edit on your own machine, you do not need this page. Use Getting started.

Mica is pre-1.0. Test upgrades and keep the version fixed in production.

Mica keeps its own Git clone of your content repository on its data volume. A save is a file in that clone that Git has not committed yet. Publish commits the chosen files as one commit and pushes it over HTTPS. The push starts your normal site build. There is no database, no content API, no read token, and no build webhook. Mica uses no API of your Git host: it uses Git over HTTPS.

To see it run first, deploy/demo-identity starts Mica and an identity provider on your machine with two commands.

What you need

  • A server with Docker and Docker Compose, and a reverse proxy that gives you HTTPS. Example: https://cms.example.com.
  • A content repository on a Git host that you reach over HTTPS. It must contain the content model at content/.mica/schema.json. Your Astro build writes that file, and you commit it. The content can be in the website repository, or in a repository of its own: see Content in its own repository.
  • An OpenID Connect provider. Mica keeps no passwords. If you have none, use Google: Sign in with Google.

The Git hosts:

  • Tested in CI: Forgejo.
  • In use, not tested automatically yet: github.com and gitlab.com.
  • Expected to work, not tested: Bitbucket, Gitea, GitHub Enterprise Server, and self-managed GitLab.

Any Git host that serves HTTPS and accepts a token as the password should work. On a host that is not tested, run the check and one real publication before editors depend on it. A host with a private certificate authority is not supported yet.

Before the first start, do these steps in this order. The self-hosted editor clones the branch of your repository. It needs the model in that branch:

  1. Build the site once (npx astro build). This writes content/.mica/schema.json.
  2. Commit the content root, content/, with .mica/schema.json. Push it to the branch that Mica uses. A model without the content gives an editor with empty collections.
  3. Make the token (step 2 below).
  4. Run the check: docker compose run --rm mica check. It is described in "The check command" below. Fix each error that it reports.
  5. Start Mica (step 6 below), sign in, and open each collection once.

SSH remotes are not supported yet. If your content stores large files with Git LFS, read Large files with Git LFS too. To edit a checkout on your machine, use the local editor.

1. Get the two files

You do not clone the Mica repository. The image is published.

mkdir -p /srv/mica && cd /srv/mica
curl -o compose.yml https://raw.githubusercontent.com/micacms/mica/main/deploy/self-hosted/compose.yml
curl -o .env https://raw.githubusercontent.com/micacms/mica/main/deploy/self-hosted/.env.example

2. Connect the repository

Mica clones your repository and pushes to it over HTTPS. Make a token for that one repository.

For GitHub, make a fine-grained personal access token for that one repository with Contents: read and write. Give it nothing else. In GitHub: Settings → Developer settings → Personal access tokens → Fine-grained tokens.

For GitLab, make a project access token with the scopes read_repository and write_repository. Mica does not use the GitLab API, so it does not need the api scope. Give the token a role that may push to the branch. GitLab protects the default branch, and the Developer role cannot push to a protected branch. Do one of these:

  • Give the token the Maintainer role.
  • Let Developers push to the branch: Settings → Repository → Protected branches.
  • Use a branch that is not protected.

Use a project access token, not a personal one, and give it a short expiry.

MICA_REPOSITORY_URL=https://github.com/acme/website.git
MICA_REPOSITORY_TOKEN=github_pat_...
MICA_REPOSITORY_BRANCH=main
  • MICA_REPOSITORY_URL is the HTTPS clone URL. Do not put credentials in it: Mica refuses https://user:token@host/…. Plain http:// works only with a loopback host, for tests. When the content is in its own repository, this is the URL of the content repository.
  • MICA_REPOSITORY_TOKEN is the password for Git over HTTPS. Mica gives it only to this host and never writes it into the clone or a log. Mica reads it at the start, so a new token needs a restart. With MICA_REPOSITORY_TOKEN_FILE instead, Mica reads the file for each Git call, and a new token needs no restart.
  • MICA_REPOSITORY_BRANCH defaults to main.
  • The content root is the directory in the repository with the .mica/schema.json, such as content. When the repository has more than one, set MICA_CONTENT_PATH to the one that Mica edits. Mica reads and writes only below the root, even if the token can write more. Thus nobody can use the token through Mica to change the website's source, its CI configuration, or its dependencies. The token itself can: see "What the repository token can reach" in Security.
  • MICA_REPOSITORY_USERNAME is optional. The default depends on the host: x-access-token for GitHub, oauth2 for GitLab, and mica for other hosts.
  • MICA_REPOSITORY_LFS_URL is optional. Set it only when your host does not serve Git LFS at the clone URL with /info/lfs. See Large files with Git LFS.
  • MICA_REPOSITORY_SYNC_SECONDS is optional. It sets how often Mica fetches the branch. The default is 60.
  • MICA_DATA_DIR is optional. It sets where Mica keeps its clone: in MICA_DATA_DIR/repository. The image uses the /data volume.
  • MICA_SITE_URL is optional. It is the address of the website, such as https://example.com. With a url in the model, an entry then shows "Open on website". See Preview changes.

3. Connect the sign-in

Mica asks an OpenID Connect provider who a person is.

MICA_OIDC_ISSUER=https://auth.example.com
MICA_OIDC_CLIENT_ID=mica
MICA_OIDC_CLIENT_SECRET=
  • MICA_OIDC_ISSUER must equal the issuer that the provider publishes at <issuer>/.well-known/openid-configuration.
  • Omit MICA_OIDC_CLIENT_SECRET for a public client that uses PKCE.
  • Register one redirect URI with the provider: <MICA_PUBLIC_URL>/api/auth/callback.
  • Mica asks for the scopes openid profile email. PKCE, state, and nonce are verified. The email address identifies an editor to the other editors.

Step by step: Sign in with Google or Other sign-in providers.

4. Say who may edit

Your provider knows many people. It does not know who may edit this website. Set at least one rule. Mica refuses to start without one.

MICA_ADMIT_GROUPS=mica-editors
MICA_ADMIT_DOMAINS=example.com
MICA_ADMIT_EMAILS=alice@gmail.com,bob@example.com
MICA_ADMIT_SUBJECTS=https://auth.example.com|a1b2c3

Each rule is a comma-separated list. One rule is enough.

  • Group: the usual choice. Put a person in the group at your provider to add an editor. The claim name is groups. If your provider uses another name, set MICA_OIDC_GROUPS_CLAIM. A Keycloak path such as /mica-editors and the bare name both match. Google sends no groups claim.
  • Domain: every address in a domain, but only addresses that the provider verified. Mica refuses a public domain such as gmail.com at start-up. With Google, the account must also belong to a Workspace whose domain is in the list (the hd claim), so that a personal Google account with a company address does not get in.
  • Emails: these addresses, if the provider verified them. The comparison ignores case. Use this rule for Google accounts. One cost: if an organisation gives the address of a former editor to a new person, that person gets access. Gmail never reuses an address.
  • Subjects: issuer|subject pairs. This is the strictest rule. A subject is never reassigned. The subject is the sub claim in the ID token. Your provider's console shows it as the user ID.

The email and domain rules need email_verified from the provider. If the provider does not send it, Mica refuses the person: an address that nobody verified is only what the person typed.

Everybody that a rule admits is an editor. There are no roles, no user table, and no invitations.

5. Set the address and the secret

MICA_PUBLIC_URL=https://cms.example.com
MICA_TRUST_PROXY=true
MICA_SESSION_SECRET=
  • MICA_PUBLIC_URL must be exactly what the browser shows, without a path. https:// turns on Secure cookies. Mica allows plain-HTTP discovery only for a provider on this machine.
  • Set MICA_TRUST_PROXY=true only behind a proxy that you control. Mica then uses the last address in X-Forwarded-For, which is the address that the proxy saw. This is correct for one proxy, such as Caddy or nginx. Behind a CDN and a proxy, all clients have the address of the CDN.
  • MICA_SESSION_SECRET must have at least 32 characters, also for a trial. Make one:
openssl rand -base64 48

Each secret can also come from a file, such as a Docker secret: MICA_SESSION_SECRET_FILE, MICA_OIDC_CLIENT_SECRET_FILE and MICA_REPOSITORY_TOKEN_FILE. Mica reads the token file for each Git call, so a short-lived token can change without a restart.

Configuration lists every variable, with its default.

The container listens on 127.0.0.1:6422. Put TLS in front of it. With Caddy, this is enough:

cms.example.com {
    reverse_proxy 127.0.0.1:6422
}

Publish answers after Git has pushed the commit. A large push, for example with new images, can take longer than a short proxy timeout. Set the read timeout of the proxy to a few minutes. In nginx, increase proxy_read_timeout.

Open only ports 80 and 443 in the firewall. Only the editors' browsers connect to Mica. The website build never connects to it.

6. Start and check

docker compose up -d
docker compose logs mica
curl -s https://cms.example.com/ready

You should see a start-up line that names the rule in force, and /ready should answer with "status": "ok". Open https://cms.example.com/admin/ and sign in. The System page shows the rule under "Who may edit".

At the first start, Mica clones the repository. Until the clone and the model are ready, /ready answers 503 with the reason cloning.

Mica stops at start-up, and lists every problem in one message, when:

  • the repository URL or the token is missing, or the URL has credentials or is not HTTPS;
  • it cannot make its first clone;
  • the repository has no content model at .mica/schema.json, or its content root is the top of the repository;
  • the clone on the volume is for another URL or branch and holds drafts (the message names the files);
  • no admission rule is set.

When the token cannot push, Mica starts and logs a warning, and check names the cause. When a clone exists, a network or token problem does not stop Mica: editors can continue, the System page shows the sync error, and Mica tries again.

The check command

check tells you if this configuration can serve the repository. It needs no browser, so you or a coding agent can run it from a shell:

docker compose run --rm mica check                   # before the first start
docker compose exec mica node dist/index.js check    # while Mica runs

It tests the configuration, read access, push rights with a dry run, the content model, Git LFS, and the data volume. It works in a temporary directory and never writes to the clone, so you can run it while Mica runs. A dry run does not see branch protection, server hooks, or other rules of the host. The first real publication shows such a refusal. Command line gives the exit codes.

Drafts, sync, and publish

A save is a draft. Drafts are files in the Git clone on the /data volume. All editors see them. On the Publish page, an editor chooses which saved changes to publish now, and the others stay saved. Publish commits the chosen changes as one commit: an index.json, its rich-text files, and its images together, so the repository never holds half an entry. A file that rich text links to goes with the commit, and Publish refuses to delete a file that rich text still links to. The editor's summary ("Say what changed") is the commit message. The editor is the author of the commit. Mica (Mica <mica@localhost>) is the committer, and the message ends with the trailer Published-with: Mica. Mica never forces a push.

Mica logs Published (with the commit and the number of files), Discarded (with the paths) and Conflict resolved (with the entry and the choice). Each line names the editor's email address. The content is never logged.

What Mica does with the repository:

  • It clones the branch into MICA_DATA_DIR/repository at the first start. The clone has one branch, and Git downloads only the files of the current tree.
  • It starts with the model in its clone, and syncs in the background. Only when the clone has no model that this version can read does the start wait for a sync.
  • It syncs at once and then at each interval: it fetches the branch and fast-forwards. It never merges, rebases, or stashes. Developers can push to the branch at any time. If a sync is still running at the next interval, Mica skips that interval.
  • It holds a version that it cannot use. If the download of the Git LFS files of the new version fails (prefetch_incomplete), Mica keeps the old version: editors can save, and Publish waits. If the new version has a content model that this Mica cannot read (model_unreadable), Mica keeps the old version and refuses every save and says why, so that a save never leaves out the new fields. Push a fix, or upgrade Mica.
  • It finds conflicts. If somebody changes an entry on the website while an editor has a draft of that entry, this is a conflict. The editor sees it on the Overview, on the Publish page, and on the entry, and chooses Keep my version or Use the website's version. While a conflict exists, Mica does not fast-forward and Publish waits.
  • It tries again. If the branch moved during a publication, Mica removes its commit, catches up, makes the commit again with the same author and message, and pushes. It tries three times. If the host cannot be reached, the publication stays committed in Mica, and the Publish page shows "Published in Mica, not on the website yet". Mica sends it later. When Mica stops, it tries once more to send it, so that a stop does not hold it back until the next start.
  • A refused push turns the publication back into drafts. If the host refuses the push, for example because of a protected branch, a server hook, or a token that cannot write, the changes are saved drafts again. The Publish page and the System page show the reason of the host. Nothing is lost.
  • It uses a new model at once. See "Upgrades and model changes" below.

The volume:

  • Mount the volume at MICA_DATA_DIR (/data in the image), never at its repository folder. Mica makes a new clone next to the old one and renames it, and a mount point cannot be renamed.
  • Run one Mica for each volume. A second Mica on the same volume waits up to 45 seconds for the first one to stop, then refuses to start. Deploy with the strategy "recreate", which stops the old container before it starts the new one, not with a rolling update. docker compose up -d does that.
  • Make the volume about twice the size of the content: the objects in .git and the files. With Git LFS, each large file is on the volume twice: in the LFS store and in the working tree. Old versions stay in the store. .git grows with each media change from the website. To make it smaller, publish or discard all drafts, stop Mica, and run docker compose run --rm mica compact. It makes a new clone with only the current files. It refuses a clone with drafts or with a publication that is not pushed yet.

The volume holds the clone and the unpublished drafts. If you lose it, unpublished work is gone. Nobody is signed out, and everything published is in the repository. Tell editors to publish often: the drafts are not an archive.

Build the website

The build reads the repository. It needs no token, no CMS address, and no network access to Mica. Each Publish is a commit, and the push starts the build. A save starts nothing.

# .github/workflows/build.yml
name: Build website
on:
  push:
    branches: [main]

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with:
          lfs: true          # only when the content uses Git LFS
          submodules: true   # only when the content is in a submodule
      - uses: actions/setup-node@v4
        with: { node-version: 24 }
      - run: npm ci
      # In CI, the build fails when the committed model is not the one in the code.
      - run: npx astro build
      # Every entry must agree with the model (needs @micacms/editor as a dev dependency).
      - run: npx mica check
      # Then upload dist/ to your host.

Without lfs: true, the build sees pointers where the media files are. If the content is in its own repository, the build must also bring the newest content: see Content in its own repository.

If your host builds from the repository, for example Vercel, Netlify, or Cloudflare Pages, you do not need this workflow.

Editors commit more frequently than developers. To keep expensive CI jobs away from content commits, filter them with paths-ignore: ["content/**"], and give the site build the opposite filter.

MICA_REPOSITORY_BRANCH can be any branch. A second Mica, with its own volume, on MICA_REPOSITORY_BRANCH=staging gives editors a staging site. You then review and merge that branch like any other change. Mica itself has no approval step.

Add the live preview

The live preview shows the drafts of the editors on the real templates of the website. It is optional. You run one more container, the preview, from the image ghcr.io/micacms/preview, and give it a name of its own. You write no Dockerfile. Mica does not start, stop or build the preview. For what the editor shows, see Preview changes.

The preview container runs the dev server of the website: MICA_PREVIEW=1 astro dev. It reads Mica's data volume, read-only. It gets none of Mica's secrets, and Mica's image does not change. A gate in the Mica process answers the requests for the preview's name, on Mica's own port, and forwards them to the dev server.

To set it up, you do three things: add the preview service and MICA_PREVIEW_UPSTREAM, make a DNS record for the preview's name, and add that name to the reverse proxy.

The website needs micaPreview() in astro.config: see Preview changes.

Add the container

Add the service preview beside mica in compose.yml, the volume preview-cache, and the variable MICA_PREVIEW_UPSTREAM to mica. Use the same version for both images:

services:
  mica:
    # ... as before
    environment:
      MICA_DATA_DIR: /data
      MICA_PREVIEW_UPSTREAM: http://preview:4321

  preview:
    image: ghcr.io/micacms/preview:0.16.0
    restart: unless-stopped
    environment:
      MICA_PUBLIC_URL: https://cms.example.com
    volumes:
      - mica-data:/mica:ro
      - preview-cache:/cache
    mem_limit: 1g

volumes:
  mica-data:
  preview-cache:
  • The preview service mounts the whole volume of Mica, read-only, at /mica. The image refuses to start when it can write to the volume. The volume preview-cache holds the caches of the package manager.
  • MICA_PUBLIC_URL of the preview service is the address of the editor. The page script of the preview answers only that editor.
  • Do not publish a port of preview. Only Mica's gate talks to it.
  • MICA_PREVIEW_UPSTREAM turns the preview on. You can set it in .env instead of compose.yml.
  • The address of the preview is the address in MICA_PUBLIC_URL with preview. in front of the host name: https://preview.cms.example.com for https://cms.example.com. Set MICA_PREVIEW_URL only for another name on the same site. It must have another host name than MICA_PUBLIC_URL, and it has no path. A name set there needs https://, except a name under localhost for a trial on this machine. Mica refuses the same host name, because a browser sends the cookies of the editor to every port of its host. MICA_PREVIEW_URL without MICA_PREVIEW_UPSTREAM is an error.
  • With the preview on, MICA_PUBLIC_URL must have a host name, not localhost, an IP address or a name of one label, such as cms. Mica refuses these, because a name beside them is on another site.
  • For a trial on one computer, set MICA_PUBLIC_URL=http://cms.localhost:6422. The preview is then http://preview.cms.localhost:6422. Browsers send every *.localhost name to the computer itself, and plain HTTP is allowed for them.

What the image does

  1. It waits for the clone of Mica, and finds the content root as Mica does. Set MICA_CONTENT_PATH when the repository has more than one.
  2. It waits up to 30 seconds for the first sync of Mica after Mica's start. Thus, when both start together, it copies the newest version of the website once.
  3. It finds the website: the nearest folder with an astro.config above the content root. Set MICA_PREVIEW_SITE_PATH to the folder of the website when there is none.
  4. It copies the repository, without .git and the content root, and links the content root of the clone in its place. Each draft is there at once.
  5. It installs the packages with the lock file at or above the website: npm ci, pnpm install --frozen-lockfile or yarn install. The caches are in /cache.
  6. It starts astro dev on port 4321.
  7. When a sync or a publication changes the commit of the clone, it copies again. It installs again only if the lock file changed, and starts the dev server again only if a file changed. Thus a Publish of content does not restart the preview.
  8. After a failure, such as an install while the npm registry is away, it tries again after 30 seconds, then after longer waits, up to ten minutes. It also tries again at once at the next change of the website.
  9. It tells Mica its state, such as installing or failed, with a reason and its last 50 lines of output, on port 4380 inside the network. It takes credentials and tokens out of the lines first. The editor's menu and the System page show them. Do not publish this port.

It removes only the files that it copied itself. Files that an install makes outside node_modules stay.

The other variables of the preview service reach the dev server, for a website that reads its own. Configuration lists the variables of the image.

The limits:

  • It uses Node 24. It works with npm, pnpm and Yarn. It does not work with Bun, and not with the Plug'n'Play of Yarn. nodeLinker: node-modules works.
  • It needs the npm registry.
  • The first start installs the packages for some minutes. With the cache, a restart takes seconds.
  • The image has no git and no compiler. A package from a Git URL does not install. A package that builds native code from source does not install.
  • In the copy, a symbolic link is a plain file, and a Git LFS file outside the content root is a pointer file.
  • When the content is in a Git submodule, the clone of Mica has no website. Such a site uses an image of your own.
  • It runs the code of the branch, as the build of the website in CI does. It has no secret of Mica.

Give the preview a name

Make a DNS record for the preview's name, such as preview.cms.example.com, or one wildcard record. Put the name in the reverse proxy, with TLS. The certificate must cover both names. The proxy sends the preview's name to the same port as the editor's name, 6422. It must pass WebSockets, because the reload of the dev server uses one, and the Host header of the browser. Caddy does this by default:

cms.example.com, preview.cms.example.com {
    reverse_proxy 127.0.0.1:6422
}

In nginx, put both names in server_name, and set proxy_set_header Host $host;, proxy_http_version 1.1, and the headers Upgrade and Connection.

Then run docker compose up -d, open an entry in the editor, and check that the page shows beside the form.

Sign-in and updates

  • The editor gets a token from Mica for the signed-in person. The token is valid for one minute. The editor posts it to the preview's origin, and the gate answers with a cookie of that origin for ten minutes. The editor renews it when the person types or clicks, at most every five minutes. A tab that nobody uses does not renew it. The reload connection of the dev server closes when the cookie ends.
  • A person whom the admission rules no longer admit keeps the preview for ten minutes at most.
  • The website never sees the cookies of Mica, and only the editor's origin can frame the preview.
  • The preview shows a draft about two seconds after typing. The autosave and the reload of the dev server take this time.
  • A change of a template shows in the preview after the next sync of Mica, every 60 seconds by default. The dev server starts again, and the page shows the change a few seconds later.

An image of your own

Use an image of your own for another Node version, a private registry or Bun. It needs the same mount and settings as the image of Mica:

  • the volume of Mica, read-only: mica-data:/mica:ro;
  • MICA_PREVIEW=1;
  • MICA_PREVIEW_EDITOR, the address of the editor, which is the value of MICA_PUBLIC_URL;
  • astro dev --host 0.0.0.0 --port 4321, so that the Mica container reaches the dev server;
  • the content folder of the website linked to /mica/repository/<content root>.

The report on port 4380 is optional. Without it, Mica sees only whether the preview answers: the menu shows Preview starting for five minutes, then Preview stopped.

This example is for an Astro project with npm:

FROM node:24-slim
WORKDIR /site
COPY package*.json ./
RUN npm ci
COPY . .
ENV MICA_PREVIEW=1
# The drafts: the content of Mica's clone, read-only, in place of the site's own.
CMD ["sh", "-c", "until [ -d /mica/repository/content ]; do sleep 2; done; rm -rf content && ln -s /mica/repository/content content && exec npx astro dev --host 0.0.0.0 --port 4321"]

The command waits for the clone of Mica. Then it links the content folder of the clone in place of the content folder of the site. repository/content is the content root of the clone: repository/<content path>. Change it for another content root, such as repository/site/content.

Add a .dockerignore file beside the Dockerfile with these lines: node_modules, dist, .astro and the content folder, such as content. Then the image gets its packages from npm ci, and its content only from Mica's volume.

Use the image as the preview service, with MICA_PREVIEW_EDITOR in place of MICA_PUBLIC_URL. After Mica clones again, for example after a change of MICA_REPOSITORY_BRANCH, restart the preview container. A change of a template shows in this preview only after a new image.

Sessions and removal of access

A session is an encrypted cookie. Mica keeps no list of sessions.

MICA_SESSION_TTL_HOURS=8
MICA_SESSION_IDLE_HOURS=4
  • A session ends after 8 hours, or after 4 hours without use. While a session is in use, Mica issues the cookie again every 5 minutes.
  • Five minutes before a session ends, the editor warns and offers to stay signed in. When a session has ended, the page stays open and offers to sign in again. The entry and settings forms keep their unsaved changes in the browser tab (sessionStorage), and after the sign-in the form offers to restore them.
  • Sign-out clears the cookie in the browser only. A copied cookie stays valid until it expires.
  • Mica re-reads the rules on each request. The rules live in .env, so restart the container after a change. The change then counts at once.
  • A change at your provider, for example removal from a group, counts from the next sign-in.
  • To remove a person at once, take them out of the rules and restart. To end all sessions, change MICA_SESSION_SECRET and restart.

Health

GET /health answers while the process runs. The image's health check uses it, so a problem at the Git host does not restart the container. GET /ready reports the clone: status is degraded when something needs a person, with reason codes such as auth_failed or pending_push, and lastSyncAt. It answers 503 only while there is no clone. Point an uptime monitor at /ready, and alert on status, not on the HTTP status code. /ready does not check the identity provider: a short outage of the provider must stop new sign-ins only. Health endpoints lists every code.

Backups

The repository is the content backup, with the history of every publish. Any clone is a full copy. To keep your own copy, mirror it on a schedule:

git clone --mirror https://github.com/acme/website.git
git --git-dir=website.git remote update --prune

To restore, clone. There is nothing to import, and you do not need Mica to read the files. With Git LFS, a mirror holds the pointers only. Also run git lfs fetch --all in the mirror, or use the backup of your Git host.

For unpublished work, back up the /data volume. First publish all drafts, or stop the container. Then copy the volume. A copy made during a Git write can be inconsistent. For the drafts only, copy the content root of the clone: MICA_DATA_DIR/repository/<content root>.

Do not run git fetch or git push through docker compose exec: that shell has no credentials and none of Mica's isolation. Use it only for GIT_OPTIONAL_LOCKS=0 git status and git diff. Without that variable, git status can lock the index and make a Mica commit fail.

Upgrades and model changes

The image tag in compose.yml is the version. Read the changelog first. Pin a version, not latest, so that an upgrade is a decision. Change the tag, then run:

docker compose pull && docker compose up -d

When you change the content model, run the Astro build, commit .mica/schema.json, and push. Mica brings the change in with its next sync and uses the new model at once; editors see a notice to load the page again. The image holds no model, so a model change never needs a new image. If this Mica cannot read the new model, it keeps the old version of the branch, and /ready has model_unreadable. Upgrade Mica, or push a model that it can read.

A Mica image can read only the models that it knows. Upgrade the image before you push a model that uses a feature of a newer version, such as a reference set.

Verify the image

The image is built for linux/amd64 and linux/arm64. Docker pulls the one for your host. The license notices are in /app/THIRD_PARTY_NOTICES.txt for the server and in /app/packages/editor-ui/dist/THIRD_PARTY_NOTICES.txt for the editor.

Each image has a keyless cosign signature from the release workflow of the Mica repository, an SBOM, and build provenance. To verify an image before you run it:

cosign verify ghcr.io/micacms/mica:<version> \
  --certificate-identity-regexp '^https://github.com/micacms/mica/\.github/workflows/release\.yml@refs/tags/v' \
  --certificate-oidc-issuer https://token.actions.githubusercontent.com

docker buildx imagetools inspect ghcr.io/micacms/mica:<version> --format '{{ json .SBOM }}'

The compose file hardens the container: a read-only root file system, /tmp in memory, no Linux capabilities, no-new-privileges, and a 512 MB memory limit. Only the /data volume is writable.

Git in the image has settings for a large repository with many images and for a slow network. You cannot change them:

  • A fetch or a push stops when it stays below 1 kB/s for 60 seconds, and after 5 minutes in all. The first clone can take 60 minutes.
  • A repack uses one thread and at most 256 MB for its window (pack.threads, pack.windowMemory). Git does not compare files larger than 16 MB with other files (core.bigFileThreshold).
  • feature.manyFiles and core.untrackedCache make the status of a large tree faster.

Troubleshooting

The System page and the log show the message of the Git host, without the token. The System page also lists the last warnings and errors since Mica started, under Warnings and errors, with email addresses hidden. Copy report adds the newest 20 of them, so an editor can send them to you without a shell on the server.

Mica stops at the start. Read docker compose logs mica, and run docker compose run --rm mica check. The message lists each problem:

  • "MICA_REPOSITORY_URL contains credentials": remove them, and set MICA_REPOSITORY_TOKEN.
  • "must be an https:// URL": use the HTTPS URL. SSH is not supported yet.
  • The content root is the top of the repository: put the content in a folder, such as content/.
  • "The clone on the data volume is for …": the branch changed, and the clone holds drafts. Start with the old branch and publish, or copy the drafts out and delete the volume. A new URL for the same repository needs nothing: Mica keeps the clone and fetches from the new URL.
  • No content model: build the Astro project, commit .mica/schema.json, and push it to MICA_REPOSITORY_BRANCH.
  • "MICA_SESSION_SECRET: is required" or "Nothing says who may edit this site": set MICA_SESSION_SECRET or an admission rule.

The repository refused the token (/ready has auth_failed). The token is wrong, expired, or revoked. Make a new token, put it in .env, and restart. If MICA_REPOSITORY_USERNAME is set to an empty value, remove it.

The token can read but not write (/ready has write_forbidden). On GitLab, add write_repository. On GitHub, give Contents: read and write.

check passes, but a publication is refused. A protected branch, a server hook, or required signed commits can cause this. With Git LFS, the host can also refuse the large files ("Authorization error"). The System page shows the reason of the host. On GitLab, use the Maintainer role, or let Developers push. The changes are drafts again: publish again after the fix.

The repository does not exist, or the token cannot see it. The URL is wrong, or the token is for a different repository. The host gives the same answer for both.

The branch was rewritten (a force push or a reset). Mica follows it at the next sync. Drafts stay drafts on the new version, and each of their entries shows as a conflict: the editor chooses to keep the draft or to take the website's version. A publication that never reached the host stays as drafts too. The log has "The branch was rewritten". After a restart, the drafts stay, but they no longer show as conflicts.

The repository cannot be reached (/ready has network or sync_failing). The host is down, DNS fails, or the container has no outgoing HTTPS. Run curl -I <url> on the server. Mica tries again at each sync.

/ready stays at cloning. The first clone cannot finish, because of the network or a very large repository. Read the log. A large clone can take minutes.

/ready has prefetch_incomplete. A sync could not download the Git LFS files of the new version. Mica keeps the old version and tries again at each sync. Check the LFS quota of your host and MICA_REPOSITORY_LFS_URL.

"A newer content model cannot be read" on the System page. The pushed .mica/schema.json is not valid, or it comes from a newer @micacms/astro than the image. Mica refuses every save until it can read the model. Correct the model and push again, or upgrade the image.

"Read-only in this version of Mica" on a collection. The site uses a field type from a newer @micacms/astro than the image. Upgrade the image to that version or newer.

"Some of these changes were edited while they were being published". A colleague saved one of the files during the publication. Look at the changes again, then publish.

The disk is full. The clone grows with each media change. See "The volume" above, and run compact.

"That account has no access to this site" after the sign-in. The person signed in, but no rule admits them. Set MICA_LOG_LEVEL=debug, restart, and sign in again: the log gives the reason. Make sure that the groups claim arrives, or add the person to MICA_ADMIT_EMAILS or MICA_ADMIT_SUBJECTS.

"That sign-in attempt expired or was not started here". The browser lost the sign-in cookie. Usually MICA_PUBLIC_URL is not the address in the browser. Make them the same.

"The identity provider could not be reached". MICA_OIDC_ISSUER is wrong, or the container cannot reach the provider. Open <issuer>/.well-known/openid-configuration from the container.

The website shows old content. The host built an older commit. Make sure that CI ran on the commit of the publication. If the content is in a submodule, make sure that the build updates it.

Next

Sign in with a Google account: Sign in with Google. Use another provider: Other sign-in providers.