Docker Compose and other CI
Running the suite outside GitHub Actions — GitLab CI, Jenkins, CircleCI, local dev. The plain Docker Compose pattern.
Outside GitHub Actions, the canonical pattern is clone, pull, compose up.
The two compose files
| File | Purpose |
|---|---|
docker-compose.yml |
Base — uses image: + build: dual mode |
docker-compose.ci.yml |
CI override — disables restart |
For CI: use both, in that order:
git clone https://github.com/php-opcua/extra-test-suite.git
cd extra-test-suite
TAG=v1.1.0 docker compose -f docker-compose.yml -f docker-compose.ci.yml pull
TAG=v1.1.0 docker compose -f docker-compose.yml -f docker-compose.ci.yml up -d
Wait for both ports
The action handles this internally. Outside the action, your CI job needs to wait:
for port in 24840 24841 24842; do
for i in $(seq 1 30); do
nc -z localhost "$port" 2>/dev/null && break
sleep 1
done
done
GitLab CI
integration-tests:
image: docker:24
services:
- docker:24-dind
variables:
TAG: v1.1.0
before_script:
- apk add --no-cache git docker-cli-compose netcat-openbsd
- git clone https://github.com/php-opcua/extra-test-suite.git /tmp/extras
- cd /tmp/extras
- docker compose -f docker-compose.yml -f docker-compose.ci.yml pull
- docker compose -f docker-compose.yml -f docker-compose.ci.yml up -d
- |
for port in 24840 24841 24842; do
for i in $(seq 1 30); do
nc -z localhost "$port" 2>/dev/null && break
sleep 1
done
done
script:
- cd "$CI_PROJECT_DIR"
- vendor/bin/pest --group=integration
after_script:
- cd /tmp/extras
- docker compose -f docker-compose.yml -f docker-compose.ci.yml down
docker:dind provides the Docker daemon the runner needs.
Jenkins (declarative pipeline)
pipeline {
agent any
environment { TAG = 'v1.1.0' }
stages {
stage('Start extras') {
steps {
sh '''
git clone https://github.com/php-opcua/extra-test-suite.git extras
cd extras
docker compose -f docker-compose.yml -f docker-compose.ci.yml pull
docker compose -f docker-compose.yml -f docker-compose.ci.yml up -d
'''
sh '''
for port in 24840 24841 24842; do
for i in $(seq 1 30); do
nc -z localhost "$port" 2>/dev/null && break
sleep 1
done
done
'''
}
}
stage('Test') {
steps {
sh 'vendor/bin/pest --group=integration'
}
}
}
post {
always {
sh '''
cd extras
docker compose -f docker-compose.yml -f docker-compose.ci.yml down
'''
}
}
}
CircleCI
jobs:
test:
machine:
image: ubuntu-2204:current
environment:
TAG: v1.1.0
steps:
- checkout
- run:
name: Start extras
command: |
git clone https://github.com/php-opcua/extra-test-suite.git /tmp/extras
cd /tmp/extras
docker compose -f docker-compose.yml -f docker-compose.ci.yml pull
docker compose -f docker-compose.yml -f docker-compose.ci.yml up -d
- run:
name: Wait for servers
command: |
for port in 24840 24841 24842; do
for i in $(seq 1 30); do
nc -z localhost "$port" 2>/dev/null && break
sleep 1
done
done
- run:
name: Tests
command: vendor/bin/pest --group=integration
- run:
name: Teardown
command: |
cd /tmp/extras
docker compose -f docker-compose.yml -f docker-compose.ci.yml down
Local development
For working on a client library that targets the suite:
git clone https://github.com/php-opcua/extra-test-suite.git
cd extra-test-suite
docker compose up -d
The base compose file uses restart: unless-stopped, so the
containers survive host reboots. Start once, leave running.
Or pull pre-built:
TAG=v1.1.0 docker compose pull
TAG=v1.1.0 docker compose up -d
Cleanup
docker compose down -v
-v removes anonymous volumes — important because
open62541-all-security's Dockerfile declares
VOLUME ["/certs"], which Docker backs with an anonymous
volume by default. Without -v that volume (and the server
cert in it) survives down, and stale fingerprints across runs
can cause cache mismatches in your tests.
Image freshness
The publish workflow tags images with both :vX.Y.Z (immutable)
and :latest (the last vX.Y.Z published). CI should pin
:vX.Y.Z for reproducibility.
Combining with the main suite
For Jenkins / GitLab / etc., run both suites in parallel:
# Pull and start main suite
git clone https://github.com/php-opcua/uanetstandard-test-suite.git /tmp/main
cd /tmp/main
docker compose -f docker-compose.yml -f docker-compose.ci.yml pull
docker compose -f docker-compose.yml -f docker-compose.ci.yml up -d
# Pull and start extras
git clone https://github.com/php-opcua/extra-test-suite.git /tmp/extras
cd /tmp/extras
docker compose -f docker-compose.yml -f docker-compose.ci.yml pull
docker compose -f docker-compose.yml -f docker-compose.ci.yml up -d
# Wait for all ports
for port in 4840 4841 4842 4843 4844 4845 4846 4847 4848 4849 4851 24840 24841 24842; do
for i in $(seq 1 60); do
nc -z localhost "$port" 2>/dev/null && break
sleep 1
done
done
# Run tests
vendor/bin/pest --group=integration
# Cleanup both
cd /tmp/main && docker compose -f docker-compose.yml -f docker-compose.ci.yml down
cd /tmp/extras && docker compose -f docker-compose.yml -f docker-compose.ci.yml down
The ports don't overlap (4840-4851 vs 24840-24842), so this just works.
Where to read next
- Forking and adding a server — to add your own services.