Every project is a group in MainPath’s GitLab at gitlab.application-platform.com with one repository per component, local-configuration for the shared editor configuration, gitops-configuration for the deployment, and gitlab-profile with the README.
orgs/<organization>/<project>/
├── backend/ # e.g. NestJS API with Dockerfile
├── <app>/ # e.g. Flutter app, several possible
├── <homepage>/ # e.g. Hugo site
├── gitops-configuration/ # deployment configuration for dev and prod
├── local-configuration/ # .vscode, .idea
└── gitlab-profile/ # group README
In the cloned project folder, symlinks point to local-configuration, so editors see the same settings for all repos. Rules for AI agents come from the agent plugin (mp agent install).
From push to running container
flowchart LR push["Push to main"] -->|"pipeline"| image["Docker image<br/>with version tag"] image -->|"write tag"| gitops["gitops-configuration<br/>versions.yaml"] gitops -->|"Ansible"| dev["Dev server"] gitops -->|"Ansible, manual job"| prod["Prod server"]
The pipeline computes the next version from the commit messages (feat: and fix: increment it), builds the Docker image, pushes it to the registry, and writes the tag into the deployment configuration for dev; the deployment repository rolls it out to the dev server with Ansible. Production receives the same tag only through the manual production job, which Git workflow and deployment describes.
The GitOps repository
gitops-configuration/
└── configurations/
├── dev/
│ ├── generated.yaml # platform: hostnames, registry, secrets (SOPS)
│ ├── custom.yaml # you: your own environment variables
│ └── versions.yaml # pipeline: deployed image versions
└── prod/ # same structure
MainPath writes generated.yaml on every project change with hostnames, registry access, and SOPS-encrypted secrets such as database passwords and JWT secrets. custom.yaml belongs to you and takes your own environment variables under additional_env_variables. The pipeline writes versions.yaml with the deployed image tag. Every commit to main, whether from the pipeline or from you, triggers the Ansible deployment of the affected environment. How these files interact with the env/ files is described in Environment variables.
What runs on the server
On the first deployment, MainPath sets up the server with Ansible: Docker, Traefik as the reverse proxy, a firewall, HTTPS certificates, and a deploy key in the authorized_keys of the specified user. Traefik routes requests by domain to the right container, including Docker Apps. Let’s Encrypt verifies the domain with TLS-ALPN on the server by default; with Cloudflare, MainPath therefore sets the DNS records without the proxy. If the server is not reachable from the internet, you can optionally tick DNS-01 in a DNS connection’s details.
The data of your backend lives outside the containers on the server: ./backend-mysql for the database, ./backend-uploads for uploads, which appear inside the container at /app/upload, and ./backend-backups for the archives of Easy Backup. A release replaces only the containers.
The pipeline uploads pure homepage projects on a web space via FTP or SFTP, without Docker, GitOps, and Ansible. Projects on a Kubernetes cluster are rolled out with Helm by GitOps; databases then live as a cloud database in the account under Cloud accounts. On classic servers, applications still run with Docker.
Generated configuration and Sentry
MainPath generates .gitlab-ci.yml, env/*.generated.env, and generated.yaml and overwrites them on every project change, so your own values belong in env/*.custom.env and custom.yaml. If you enable Sentry for a component under Features, MainPath creates the Sentry project in the account connected under Connections and writes the DSN into this configuration, for apps into env/shared.generated.env, for example. Operations describes how to work with them.