# Architecture

GitLab pipelines build Docker images, a GitOps repository records which version runs where, and Ansible brings that state to servers that the platform has set up with Traefik, a firewall, and HTTPS.

> Source: https://www.application-platform.com/en/docs/architecture/

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

```mermaid
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]({{< relref "git-workflow" >}}) 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]({{< relref "environment-setup" >}}).

## 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]({{< relref "hosted-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]({{< relref "webspace-ftp" >}}) 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]({{< relref "operations" >}}) describes how to work with them.

