Every project is a group in the platform’s GitLab at gitlab.application-platform.com with one repository per component, local-configuration for the shared agent 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/ # AGENTS.md, CLAUDE.md, .cursor, .claude, .codex, .vscode
└── gitlab-profile/ # group README
In the cloned project folder, symlinks point to local-configuration, so AI agents and editors see the same rules for all repos.
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
The platform 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, the platform 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. For Let’s Encrypt, the server has to be reachable directly; with Cloudflare, the platform therefore sets the DNS records without the proxy.
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. A Kubernetes access under Connections is only saved for now; all applications run via Docker on the server.
Generated configuration and Sentry
The platform 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, the platform 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.