English | Русский
A single gitlab-ci.yml that turns a GitLab pipeline into a remote control panel for a stand — a server running a set of Docker Compose services — over SSH. No agents on the target host: everything is plain ssh + rsync + docker compose from a CI runner.
To use it, drop the file into your project as .gitlab-ci.yml and set two CI/CD variables (see Configuration).
.
├── .gitlab-ci.yml # this pipeline
├── services/ # services/<name>/docker-compose.yml — one directory per service
├── scripts/ # helper shell scripts, mirrored to the stand
└── backups/ # optional seed content for the stand's backups directory
| Job | Mode | What it does |
|---|---|---|
auto_deploy_services |
automatic on push | git-diff based sync: rsync new and changed services, restart the changed ones, down + remove the deleted ones, then a final status report per service |
auto_deploy_scripts |
automatic on push | mirror scripts/ to the stand, including file deletions |
start_all_services / stop_all_services / restart_all_services |
manual | iterate BASE_PATH, docker compose up/down/restart in every directory with a compose file |
status_all_services |
manual | per-service status, ps and last 30 log lines for stopped ones, plus disk / memory / container table |
| Job | What it does |
|---|---|
push_services_only |
full stand reinstall: stop and remove all containers and images, recreate BASE_PATH, rsync services/, pull and start everything, then verify status |
push_scripts_only / push_backups_only |
replace the stand's scripts/ / backups/ directory with the repo copy |
delete_docker_containers / delete_docker_images / delete_docker_volumes |
targeted host cleanup: containers / images / named volumes |
delete_services_dir |
wipe and recreate BASE_PATH |
DROP_FULL_STAND |
all of the above in one button |
backup_full_stand |
full backup: tar archives of every service and of scripts/, docker save of every running container's image, named-volume dumps — each with a .sha256 checksum and a jq-generated backup_manifest.json |
Each generator job inspects the stand and produces a child pipeline (artifact → trigger:), so buttons appear per discovered service/script:
| Pair | Result |
|---|---|
generate_logs_pipeline → trigger_logs_pipeline |
a "logs" button per service: last 100 lines + live follow |
generate_services_pipeline → trigger_services_pipeline |
a manual deploy button per service |
generate_scripts_pipeline → trigger_scripts_pipeline |
a run button per *.sh script found on the stand (executed with sudo) |
auto_deploy_* jobs compute a diff against a BASE_REF:
- merge request pipeline →
origin/${CI_MERGE_REQUEST_TARGET_BRANCH_NAME}; - push with a valid
CI_COMMIT_BEFORE_SHA→ that SHA; - fallback →
HEAD~1if it exists; - otherwise the diff is skipped with a warning (new/deleted service sync still runs — it doesn't depend on the diff).
GIT_DEPTH: 0 is set so the MR target branch is always fetchable.
Pipeline variables (defaults in the file, override per environment/branch):
| Variable | Default | Purpose |
|---|---|---|
STAND |
stand |
stand name: SSH host and deploy subdirectory |
SSH_HOST |
$STAND |
host to connect to |
BASE_PATH |
/opt/apps/${STAND} |
services root; each subdirectory is one compose service |
SCRIPTS_PATH |
/opt/scripts |
helper scripts directory on the stand |
BACKUPS_ROOT |
/opt/backups |
backups root on the stand |
REPO_SERVICES_PATH / REPO_SCRIPTS_PATH / REPO_BACKUPS_PATH |
services / scripts / backups |
repository directories to sync |
RSYNC_EXCLUDES |
.git, CI files, .md |
exclusions applied to every rsync |
Required project CI/CD variables (Settings → CI/CD → Variables, masked where possible):
| Variable | Purpose |
|---|---|
DEPLOY_USER |
SSH user on the stand |
SSH_PRIVATE_KEY |
private key allowed to log in to the stand |
The default image must contain openssh-client, rsync and jq. The file ships with a placeholder ubuntu:24.04 — replace it with an image from your registry (the same image name is embedded in the generated child pipelines).
- The destructive jobs (
DROP_FULL_STAND,delete_*,push_services_only) operate on the whole host — they stop/remove all containers, images and volumes, not just "own" ones. They arewhen: manualbuttons by design; protect your branches and restrict who can press them. - Host keys are accepted via
ssh-keyscan(TOFU) and rsync usesStrictHostKeyChecking=no. Acceptable for an internal lab stand; for anything stricter, pre-seedknown_hosts. - The model assumes passwordless
sudofor the deploy user on the stand.
- GitLab with CI and a runner with the Docker executor
- Network access from the runner to the stand over SSH (port 22)