This document contains manual, step-by-step installation instructions for setting up CloudLinux mirrors. It mirrors what the Ansible playbooks in this repository do, but in a copy/paste friendly format.
Current support: CloudLinux 10 is supported now. Support for older versions (9/8/...) will be enabled gradually.
If you prefer automation, use the corresponding playbooks:
ansible/combined-mirror/ansible/complete-swng-rsync/ansible/specific-version-rsync(Recomended)/ansible/yum-reposync/
- Prepare Storage
- 1. Combined-mirror: Mirroring SWNG + CloudLinux with RSync
- 2. Complete-swng-rsync: Mirroring the Complete SWNG Repository with RSync
- 3. Specific-version-rsync: Mirroring Specific SWNG Versions with RSync (Recommended)
- yum-reposync: Mirroring SWNG repositories with
reposync - Adding /healthcheck to Existing Manual Installations
- Advanced Options
Short, direct examples. Adjust paths as needed.
Ensure you have sufficient disk space. Repository mirrors can require several hundred gigabytes to multiple terabytes depending on what you mirror.
Storage recommendation: Use a dedicated disk or partition for mirror storage to avoid filling the root filesystem and to improve I/O performance.
Sizing guidance:
- SWNG mirror size is approximately 500 GB
- The full
repo.cloudlinux.com(CloudLinux repository) is 3+ TB - In most cases, sync
repo.cloudlinux.comonly partially (only the repositories you actually need)
Recommendation: For most production environments, prioritize mirroring SWNG (the main operational repository) as it contains all packages needed for operational systems.
This manual example mirrors what ansible/combined-mirror does in its default ("combined") mode:
- Mirror paths under
/var/www/mirrors/ - RSync sources
SWNG/andCLOUDLINUX/ - A single systemd timer/service named
cloudlinux-complete-mirror - Nginx serving
/cloudlinux/and/swng/(with optional Let's Encrypt HTTPS)
Check available disk space (combined mirrors may require hundreds of GB to 1+ TB).
df -hCreate mirror directories and run the initial sync (this may take hours).
mkdir -p /var/www/mirrors/swng /var/www/mirrors/cloudlinux
# Initial CloudLinux sync
rsync -av --delete \
--progress \
--log-file=/var/log/cloudlinux-mirror.log \
rsync://rsync.upstream.cloudlinux.com/CLOUDLINUX/ \
/var/www/mirrors/cloudlinux/
# Initial SWNG sync
rsync -av --delete \
--progress \
--log-file=/var/log/swng-mirror.log \
rsync://rsync.upstream.cloudlinux.com/SWNG/ \
/var/www/mirrors/swng/Create /etc/systemd/system/cloudlinux-complete-mirror.service:
[Unit]
Description=Sync Complete CloudLinux and SWNG Mirrors
After=network.target
[Service]
Type=oneshot
ExecStart=/bin/bash -c '/usr/bin/rsync -av --delete rsync://rsync.upstream.cloudlinux.com/CLOUDLINUX/ /var/www/mirrors/cloudlinux/ && /usr/bin/rsync -av --delete rsync://rsync.upstream.cloudlinux.com/SWNG/ /var/www/mirrors/swng/'
StandardOutput=append:/var/log/cloudlinux-complete-mirror.log
StandardError=append:/var/log/cloudlinux-complete-mirror.logCreate /etc/systemd/system/cloudlinux-complete-mirror.timer:
[Unit]
Description=Run Complete CloudLinux and SWNG Mirror Sync Every 4 Hours
Requires=cloudlinux-complete-mirror.service
[Timer]
OnCalendar=*-*-* 00,04,08,12,16,20:00:00
Persistent=true
[Install]
WantedBy=timers.targetEnable and start the timer:
systemctl daemon-reload
systemctl enable cloudlinux-complete-mirror.timer
systemctl start cloudlinux-complete-mirror.timer
# Check timer status
systemctl status cloudlinux-complete-mirror.timer
systemctl list-timers cloudlinux-complete-mirror.timerInstall Nginx with your OS package manager, then create an HTTP config at /etc/nginx/conf.d/combined-mirror.conf and start Nginx:
systemctl enable --now nginx
systemctl status nginx
# Test local HTTP access (works if you use the non-HTTPS config below)
curl -I http://localhost/cloudlinux/ || true
curl -I http://localhost/swng/ || trueOption A (no HTTPS): serve over HTTP only
Create /etc/nginx/conf.d/combined-mirror.conf:
server {
listen 80;
listen [::]:80;
server_name _;
root /var/www/mirrors;
index index.html;
# Enable directory listing
autoindex on;
autoindex_exact_size off;
autoindex_localtime on;
access_log /var/log/nginx/combined-mirror-access.log;
error_log /var/log/nginx/combined-mirror-error.log;
location /cloudlinux/ {
alias /var/www/mirrors/cloudlinux/;
autoindex on;
}
location /swng/ {
alias /var/www/mirrors/swng/;
autoindex on;
}
location / {
try_files $uri $uri/ =404;
}
client_max_body_size 0;
}Reload Nginx:
nginx -t && systemctl reload nginxOption B (playbook default): Let's Encrypt HTTPS + HTTP→HTTPS redirect
Requirements:
mirror.example.commust resolve to the mirror server- inbound TCP
80must be reachable during certificate issuance
Obtain a certificate (standalone method, like the playbook default):
# Install certbot packages (package names may vary by distro)
dnf install -y certbot python3-certbot-nginx
# Stop nginx temporarily for standalone auth
systemctl stop nginx || true
certbot certonly --standalone --preferred-challenges http \
--non-interactive --agree-tos \
--email admin@mirror.example.com \
-d mirror.example.com
systemctl start nginxCreate /etc/nginx/conf.d/combined-mirror.conf (HTTP→HTTPS redirect + ACME location):
server {
listen 80;
listen [::]:80;
server_name mirror.example.com;
location /.well-known/acme-challenge/ {
root /var/www/mirrors/acme;
}
location / {
return 301 https://$server_name$request_uri;
}
}Create /etc/nginx/conf.d/combined-mirror-https.conf:
server {
listen 443 ssl http2;
listen [::]:443 ssl http2;
server_name mirror.example.com;
root /var/www/mirrors;
index index.html;
ssl_certificate /etc/letsencrypt/live/mirror.example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/mirror.example.com/privkey.pem;
# SSL Security Settings
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers HIGH:!aNULL:!MD5;
ssl_prefer_server_ciphers on;
ssl_session_cache shared:SSL:10m;
ssl_session_timeout 10m;
# Enable directory listing
autoindex on;
autoindex_exact_size off;
autoindex_localtime on;
access_log /var/log/nginx/combined-mirror-https-access.log;
error_log /var/log/nginx/combined-mirror-https-error.log;
location /cloudlinux/ {
alias /var/www/mirrors/cloudlinux/;
autoindex on;
}
location /swng/ {
alias /var/www/mirrors/swng/;
autoindex on;
}
location / {
try_files $uri $uri/ =404;
}
client_max_body_size 0;
}Reload Nginx and verify:
nginx -t && systemctl reload nginx
curl -I https://mirror.example.com/cloudlinux/ || true
curl -I https://mirror.example.com/swng/ || trueFor the full automation (including optional separate timers, Nginx templating, and certbot renewal), see ansible/combined-mirror/README.md.
This manual example mirrors what ansible/complete-swng-rsync does in its default mode:
- Mirror path:
/var/www/mirrors/swng - RSync source:
rsync://rsync.upstream.cloudlinux.com/SWNG/ - systemd units:
swng-mirror.service+swng-mirror.timer - Nginx serving the mirror under
/swng/(with optional Let's Encrypt HTTPS)
Check available disk space.
df -hCreate mirror directory and run the initial sync (this may take hours).
mkdir -p /var/www/mirrors/swng
rsync -av --delete \
--progress \
--log-file=/var/log/swng-mirror.log \
rsync://rsync.upstream.cloudlinux.com/SWNG/ \
/var/www/mirrors/swng/Create /etc/systemd/system/swng-mirror.service:
[Unit]
Description=Sync Complete SWNG Repository Mirror
After=network.target
[Service]
Type=oneshot
ExecStart=/usr/bin/rsync -av --delete rsync://rsync.upstream.cloudlinux.com/SWNG/ /var/www/mirrors/swng/
StandardOutput=append:/var/log/swng-mirror.log
StandardError=append:/var/log/swng-mirror.logCreate /etc/systemd/system/swng-mirror.timer:
[Unit]
Description=Run Complete SWNG Mirror Sync Every 4 Hours
Requires=swng-mirror.service
[Timer]
OnCalendar=*-*-* 00,04,08,12,16,20:00:00
Persistent=true
[Install]
WantedBy=timers.targetEnable and start the timer:
systemctl daemon-reload
systemctl enable swng-mirror.timer
systemctl start swng-mirror.timer
# Check timer status
systemctl status swng-mirror.timer
systemctl list-timers swng-mirror.timerInstall Nginx with your OS package manager.
systemctl enable --now nginx
systemctl status nginxOption A (no HTTPS): serve over HTTP only
Create /etc/nginx/conf.d/swng-mirror.conf:
server {
listen 80;
listen [::]:80;
server_name mirror.example.com;
root /var/www/mirrors/swng;
index index.html;
autoindex on;
autoindex_exact_size off;
autoindex_localtime on;
access_log /var/log/nginx/swng-mirror-access.log;
error_log /var/log/nginx/swng-mirror-error.log;
location / {
try_files $uri $uri/ =404;
}
# Optional SWNG prefix for mixed setups
location = /swng {
return 301 /swng/;
}
location /swng/ {
alias /var/www/mirrors/swng/;
autoindex on;
}
client_max_body_size 0;
}Reload Nginx and verify:
nginx -t && systemctl reload nginx
curl -I http://localhost/swng/ || trueOption B (playbook default): Let's Encrypt HTTPS + HTTP→HTTPS redirect
Requirements:
mirror.example.commust resolve to the mirror server- inbound TCP
80must be reachable during certificate issuance
Obtain a certificate (standalone method, like the playbook default):
dnf install -y certbot python3-certbot-nginx
systemctl stop nginx || true
certbot certonly --standalone --preferred-challenges http \
--non-interactive --agree-tos \
--email admin@mirror.example.com \
-d mirror.example.com
systemctl start nginxCreate /etc/nginx/conf.d/swng-mirror.conf (HTTP→HTTPS redirect + ACME location):
server {
listen 80;
listen [::]:80;
server_name mirror.example.com;
location /.well-known/acme-challenge/ {
root /var/www/mirrors/acme;
}
# Normalize SWNG path (no trailing slash)
location = /swng {
return 301 https://$server_name/swng/;
}
location / {
return 301 https://$server_name$request_uri;
}
}Create /etc/nginx/conf.d/swng-mirror-https.conf:
server {
listen 443 ssl http2;
listen [::]:443 ssl http2;
server_name mirror.example.com;
root /var/www/mirrors/swng;
index index.html;
ssl_certificate /etc/letsencrypt/live/mirror.example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/mirror.example.com/privkey.pem;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers HIGH:!aNULL:!MD5;
ssl_prefer_server_ciphers on;
ssl_session_cache shared:SSL:10m;
ssl_session_timeout 10m;
autoindex on;
autoindex_exact_size off;
autoindex_localtime on;
access_log /var/log/nginx/swng-mirror-https-access.log;
error_log /var/log/nginx/swng-mirror-https-error.log;
location / {
try_files $uri $uri/ =404;
}
location = /swng {
return 301 /swng/;
}
location /swng/ {
alias /var/www/mirrors/swng/;
autoindex on;
}
client_max_body_size 0;
}Reload Nginx and verify:
nginx -t && systemctl reload nginx
curl -I https://mirror.example.com/swng/ || trueFor automation (systemd timers, Nginx templating, and certbot renewal), see ansible/complete-swng-rsync/README.md.
This manual example mirrors what ansible/specific-version-rsync(Recomended) does in its default mode:
- CloudLinux version mirrored: 10 (8/9 support is coming soon)
- Mirror path:
/var/www/mirrors/swng/10 - RSync source:
rsync://rsync.upstream.cloudlinux.com/SWNG/10/ - systemd units:
swng-10-mirror.service+swng-10-mirror.timer(every 6 hours) - Nginx serving the mirror under
/swng/so that version content is available at/swng/10/(with optional Let's Encrypt HTTPS)
df -h
mkdir -p /var/www/mirrors/swng/10
rsync -av --delete \
--progress \
--log-file=/var/log/swng-10-mirror.log \
rsync://rsync.upstream.cloudlinux.com/SWNG/10/ \
/var/www/mirrors/swng/10/Create /etc/systemd/system/swng-10-mirror.service:
[Unit]
Description=Sync CloudLinux 10 SWNG Repository Mirror
After=network.target
[Service]
Type=oneshot
ExecStart=/usr/bin/rsync -av --delete rsync://rsync.upstream.cloudlinux.com/SWNG/10/ /var/www/mirrors/swng/10/
StandardOutput=append:/var/log/swng-10-mirror.log
StandardError=append:/var/log/swng-10-mirror.logCreate /etc/systemd/system/swng-10-mirror.timer:
[Unit]
Description=Run CloudLinux 10 SWNG Mirror Sync Every 6 Hours
Requires=swng-10-mirror.service
[Timer]
OnCalendar=*-*-* 00,06,12,18:00:00
Persistent=true
[Install]
WantedBy=timers.targetEnable and start the timer:
systemctl daemon-reload
systemctl enable swng-10-mirror.timer
systemctl start swng-10-mirror.timer
systemctl status swng-10-mirror.timer
systemctl list-timers swng-10-mirror.timerInstall and start Nginx:
systemctl enable --now nginx
systemctl status nginxOption A (no HTTPS): serve over HTTP only
Create /etc/nginx/conf.d/swng-mirror.conf:
server {
listen 80;
listen [::]:80;
server_name _;
root /var/www/mirrors;
index index.html;
autoindex on;
autoindex_exact_size off;
autoindex_localtime on;
access_log /var/log/nginx/swng-10-mirror-access.log;
error_log /var/log/nginx/swng-10-mirror-error.log;
location / {
try_files $uri $uri/ =404;
}
# Optional SWNG prefix for mixed setups
location = /swng {
return 301 /swng/;
}
location /swng/ {
alias /var/www/mirrors/swng/;
autoindex on;
}
client_max_body_size 0;
}Reload Nginx and verify:
nginx -t && systemctl reload nginx
curl -I http://localhost/swng/10/ || trueOption B (playbook default): Let's Encrypt HTTPS + HTTP→HTTPS redirect
Requirements:
mirror.example.commust resolve to the mirror server- inbound TCP
80must be reachable during certificate issuance
dnf install -y certbot python3-certbot-nginx
systemctl stop nginx || true
certbot certonly --standalone --preferred-challenges http \
--non-interactive --agree-tos \
--email admin@mirror.example.com \
-d mirror.example.com
systemctl start nginxCreate /etc/nginx/conf.d/swng-mirror.conf (HTTP→HTTPS redirect + ACME location):
server {
listen 80;
listen [::]:80;
server_name mirror.example.com;
location /.well-known/acme-challenge/ {
root /var/www/mirrors/acme;
}
# Normalize SWNG path (no trailing slash)
location = /swng {
return 301 https://$server_name/swng/;
}
location / {
return 301 https://$server_name$request_uri;
}
}Create /etc/nginx/conf.d/swng-mirror-https.conf:
server {
listen 443 ssl http2;
listen [::]:443 ssl http2;
server_name mirror.example.com;
root /var/www/mirrors;
index index.html;
ssl_certificate /etc/letsencrypt/live/mirror.example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/mirror.example.com/privkey.pem;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers HIGH:!aNULL:!MD5;
ssl_prefer_server_ciphers on;
ssl_session_cache shared:SSL:10m;
ssl_session_timeout 10m;
autoindex on;
autoindex_exact_size off;
autoindex_localtime on;
access_log /var/log/nginx/swng-10-mirror-https-access.log;
error_log /var/log/nginx/swng-10-mirror-https-error.log;
location / {
try_files $uri $uri/ =404;
}
location = /swng {
return 301 /swng/;
}
location /swng/ {
alias /var/www/mirrors/swng/;
autoindex on;
}
client_max_body_size 0;
}Reload Nginx and verify:
nginx -t && systemctl reload nginx
curl -I https://mirror.example.com/swng/10/ || trueFor automation (systemd timers, Nginx templating, and certbot renewal), see ansible/specific-version-rsync(Recomended)/README.md.
This manual example mirrors what ansible/yum-reposync does:
- Mirrors selected SWNG repositories using
reposync(yum-utils) - Generates metadata with
createrepo - Uses a systemd service/timer named
swng-reposync - Serves the mirror via Nginx (and optionally Let's Encrypt HTTPS)
The playbook installs yum-utils and createrepo (package names may vary by distro).
dnf -y install yum-utils createrepo || yum -y install yum-utils createrepoCreate /etc/yum.repos.d/swng-upstream.repo:
# SWNG Repository Configuration for upstream.cloudlinux.com
# Source: https://upstream.cloudlinux.com/swng/
[SWNG-10-x86_64]
name=SWNG-10-x86_64
baseurl=https://upstream.cloudlinux.com/swng/10/x86_64/
enabled=1
skip_if_unavailable=1
gpgcheck=0Notes:
- The playbook disables GPG checks by default (
gpgcheck=0). If you want to enable GPG verification, setgpgcheck=1and configure the appropriate key. - For modular content,
reposyncmay requiremodule_platform_id(the playbook uses--setopt=module_platform_id=...when configured).
mkdir -p /var/www/mirrors/swng
# Initial sync of enabled repos
reposync -p /var/www/mirrors/swng/ --repo SWNG-10-x86_64 --setopt=module_platform_id=platform:el10
# Generate metadata (required)
createrepo /var/www/mirrors/swng/SWNG-10-x86_64/Create /etc/systemd/system/swng-reposync.service:
[Unit]
Description=Sync SWNG Repositories with reposync
After=network.target
[Service]
Type=oneshot
ExecStart=/usr/bin/reposync -p /var/www/mirrors/swng/ --repo SWNG-10-x86_64 --setopt=module_platform_id=platform:el10
ExecStartPost=/usr/bin/createrepo /var/www/mirrors/swng/SWNG-10-x86_64/
StandardOutput=append:/var/log/swng-reposync.log
StandardError=append:/var/log/swng-reposync.logCreate /etc/systemd/system/swng-reposync.timer:
[Unit]
Description=Run SWNG reposync Every 6 Hours
Requires=swng-reposync.service
[Timer]
OnCalendar=*-*-* 00,06,12,18:00:00
Persistent=true
[Install]
WantedBy=timers.targetEnable and start:
systemctl daemon-reload
systemctl enable swng-reposync.timer
systemctl start swng-reposync.timer
systemctl status swng-reposync.timer
systemctl list-timers swng-reposync.timerInstall and start Nginx:
systemctl enable --now nginx
systemctl status nginxCreate /etc/nginx/conf.d/swng-reposync-mirror.conf:
server {
listen 80;
listen [::]:80;
server_name mirror.example.com;
root /var/www/mirrors/swng;
index index.html;
autoindex on;
autoindex_exact_size off;
autoindex_localtime on;
access_log /var/log/nginx/swng-reposync-mirror-access.log;
error_log /var/log/nginx/swng-reposync-mirror-error.log;
location / {
try_files $uri $uri/ =404;
}
# Optional SWNG prefix for mixed setups
location /swng/ {
alias /var/www/mirrors/swng/;
autoindex on;
}
location = /swng {
return 301 /swng/;
}
client_max_body_size 0;
}Reload Nginx and verify:
nginx -t && systemctl reload nginx
curl -I http://localhost/swng/ || trueOptional HTTPS (playbook default):
- The playbook can obtain a Let's Encrypt certificate using certbot and add
swng-reposync-mirror-https.conf. - For the full automation (including certbot and renewal), see
ansible/yum-reposync/README.md.
If you set up your mirror manually before the /healthcheck endpoint contract was required, follow these steps to add it to an existing installation. Paths below assume the defaults from sections 1-4 — adjust if you customized your layout.
# RedHat-family (CloudLinux, AlmaLinux, RHEL, Rocky)
dnf install -y epel-release
dnf install -y python3-dotenv
# Optional but recommended on EL with enforcing SELinux (see Step 6)
dnf install -y policycoreutils-python-utils
# Debian/Ubuntu
apt update && apt install -y python3-dotenvmkdir -p /opt/healthcheck /var/www
curl -fsSL https://raw.githubusercontent.com/cloudlinux/cloudlinux-mirrors/main/ansible/healthcheck/healthcheck_update.py \
-o /opt/healthcheck/healthcheck_update.py
chmod 0755 /opt/healthcheck/healthcheck_update.py
cat > /opt/healthcheck/.env <<'EOF'
HEALTHCHECK_JSON=/var/www/healthcheck.json
HEALTHCHECK_HTML=/var/www/healthcheck.html
EOFThis makes /healthcheck.json reachable immediately, before your first post-migration sync:
/usr/bin/python3 /opt/healthcheck/healthcheck_update.py \
--service sync --field repo --value swng.cloudlinux.com --status PENDING
cat /var/www/healthcheck.jsonEdit your nginx vhost(s) — typically /etc/nginx/conf.d/swng-mirror.conf and/or swng-mirror-https.conf — and add inside each server { ... } block:
# Health-check for cl-mirrors mirrorservice
location = /healthcheck {
alias /var/www/healthcheck.html;
default_type text/html;
}
location = /healthcheck.json {
alias /var/www/healthcheck.json;
default_type application/json;
add_header Cache-Control "no-store";
}Validate and reload:
nginx -t && systemctl reload nginxEdit your systemd sync unit (the one that runs rsync or reposync) and add the ExecStartPost= line to the [Service] block. The exact unit file depends on your setup:
| Setup | Unit file |
|---|---|
| Combined mirror (one service) | /etc/systemd/system/cloudlinux-complete-mirror.service |
| Separate SWNG service | /etc/systemd/system/swng-mirror.service |
| Per-version SWNG | /etc/systemd/system/swng-<VERSION>-mirror.service |
| reposync | /etc/systemd/system/swng-reposync.service |
Add to [Service]:
ExecStartPost=/usr/bin/python3 /opt/healthcheck/healthcheck_update.py --service sync --field repo --value swng.cloudlinux.com --status OKThen:
systemctl daemon-reloadRun your sync service once so ExecStartPost fires and flips the status:
# choose the service unit matching your setup
systemctl start swng-mirror.service # or cloudlinux-complete-mirror.service /
# swng-<version>-mirror.service /
# swng-reposync.serviceVerify:
curl -s https://<your-mirror>/healthcheck.jsonExpected:
{
"healthcheck_update": "YYYY/MM/DD HH:MM:SS",
"sync_status": [
{"repo": "swng.cloudlinux.com", "status": "OK", "time": "YYYY/MM/DD HH:MM:SS"}
]
}If status stays PENDING after a sync run, check that your service unit has the ExecStartPost= line and that daemon-reload was issued:
systemctl cat <your-sync>.service | grep ExecStartPost
journalctl -u <your-sync>.service --since '5 min ago'If a sync is interrupted, RSync will resume from where it left off on the next run. The --partial option can help with large files:
The --partial option tells rsync to save partially downloaded files and download them later, which saves time and traffic when dealing with large ISOs/archives.
rsync -av --delete --partial rsync://rsync.upstream.cloudlinux.com/CLOUDLINUX/ /var/www/mirrors/cloudlinux/Sync only specific versions or architectures:
# Sync only CloudLinux 10
rsync -av --delete \
--include="10/**" \
--exclude="*" \
rsync://rsync.upstream.cloudlinux.com/CLOUDLINUX/ \
/var/www/mirrors/cloudlinux/
# Sync only x86_64 architecture
rsync -av --delete \
--include="*/x86_64/**" \
--exclude="*" \
rsync://rsync.upstream.cloudlinux.com/CLOUDLINUX/ \
/var/www/mirrors/cloudlinux/Limit bandwidth usage during sync:
rsync -av --delete --bwlimit=10000 \
rsync://rsync.upstream.cloudlinux.com/CLOUDLINUX/ \
/var/www/mirrors/cloudlinux/Enable compression (useful for slow connections):
rsync -avz --delete \
rsync://rsync.upstream.cloudlinux.com/CLOUDLINUX/ \
/var/www/mirrors/cloudlinux/