All CI/CD services are defined in a single docker-compose.yaml and managed with podman compose.
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
files/ ConventionEach 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.
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
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.
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
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.
# 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
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.