Docker Compose Setup

All CI/CD services are defined in a single docker-compose.yaml and managed with podman compose.

Folder Structure

opt/docker-compose/cicd/
├── docker-compose.yaml        # Service definitions, networks, volumes
├── env.txt                    # Shared environment variables (credentials, LDAP config)
├── restart.sh                 # Helper functions (restart, reset LDAP)
├── phpldapadmin/              # Custom image: OpenLDAP + phpLDAPadmin
│   ├── Dockerfile
│   ├── variables.env          # Laravel .env (baked into image)
│   └── files/                 # Copied into image at build time
│       ├── opt/bin/           # entrypoint.sh, render_variables.py
│       ├── opt/etc/openldap/  # slapd.conf.jinja2, bootstrap.ldif.jinja2, variables.env.jinja2
│       └── app/templates/     # phpLDAPadmin user creation template
├── forgejo/                   # Custom image: rootless Forgejo + DB init
│   ├── Dockerfile
│   └── files/
│       └── opt/bin/           # Entrypoint wrapper, DB creation script
├── jenkins/                   # Custom image: Jenkins + plugins + LDAP
│   ├── Dockerfile
│   └── files/
│       └── usr/share/jenkins/ref/
│           ├── init.groovy.d/         # Scripts that run on every boot
│           └── init.groovy.disabled/  # Scripts available but inactive
└── postgres/                  # No custom image
    └── README.md

The files/ Convention

Each service with a custom Dockerfile has a files/ subdirectory. Its contents are copied into the image with COPY files/ /, meaning the directory structure inside files/ mirrors the absolute paths in the container.

For example, phpldapadmin/files/opt/bin/entrypoint.sh ends up at /opt/bin/entrypoint.sh inside the container.

Jinja2 Templating

The phpldapadmin container uses jinja2 templates (.jinja2 files) for configuration that depends on environment variables. At container startup, render_variables.py renders these templates using values from env.txt (loaded via env_file in docker-compose). This avoids hardcoding domain names, passwords, and other environment-specific values into the image.

Templated files: - slapd.conf.jinja2/etc/openldap/slapd.conf - bootstrap.ldif.jinja2/opt/etc/openldap/bootstrap.ldif - variables.env.jinja2/app/.env

Services and Networking

All containers share a dedicated bridge network with static IP addresses:

Service IP Internal Port Purpose
postgres 172.20.0.12 5432 PostgreSQL database
forgejo 172.20.0.10 3000 Git hosting
phpldapadmin 172.20.0.13 8080 (web), 389 (ldap) LDAP server + web UI
jenkins 172.20.0.14 8080 Build server

Nginx (running on the host, not in a container) proxies to these IPs. Forgejo also exposes port 2222 on the host for SSH git access.

Container Dependencies

postgres ← forgejo (creates DB on startup, then connects)
phpldapadmin ← jenkins (LDAP authentication)
phpldapadmin ← forgejo (LDAP authentication, configured via UI)

There are no explicit depends_on directives. Each service handles waiting for its dependencies: - Forgejo's entrypoint wrapper waits up to 60s for PostgreSQL before creating the database - Jenkins retries LDAP connections internally - phpLDAPadmin's entrypoint waits for slapd to be ready before bootstrapping

Shared Variables (env.txt)

The env.txt file is loaded by services that need shared credentials and LDAP configuration:

CI_DOMAIN=pathoris
CI_ROOT_DOMAIN=de
POSTGRES_USER=postgres
POSTGRES_PASSWORD=...

LDAP_ORGANISATION="Pathoris CI"
LDAP_ADMIN_USER=admin
LDAP_ADMIN_PASSWORD=...
LDAP_BASE_DN=dc=pathoris,dc=de
...

This is referenced via env_file: env.txt in docker-compose. Services that load it: postgres, forgejo, phpldapadmin.

Note: The file is named env.txt, not .env. Docker/Podman automatically loads .env files with special semantics — using a different name avoids that implicit behavior.

Building and Running

# Build all custom images and start
podman compose up -d --build

# Rebuild a single service
podman compose up -d --build phpldapadmin

# View logs
podman compose logs -f phpldapadmin

# Stop everything
podman compose down

Data Persistence

Each service stores its data on the host under /var/lib/cicd/:

Service Host Path Container Path
postgres /var/lib/cicd/postgres /var/lib/postgresql/data
forgejo /var/lib/cicd/forgejo/data /data
forgejo /var/lib/cicd/forgejo/gitea /var/lib/gitea
ldap (slapd) /var/lib/cicd/ldap/data /var/lib/openldap/openldap-data
jenkins /var/lib/cicd/jenkins /var/jenkins_home

SELinux volume labels: Use :Z (private) for most volumes. Use :z (shared) for LDAP data — the uppercase variant can cause permission issues with slapd.