Docker Compose Blue-Green Release on RHEL 9 VPS

Use case and release plan
A docker compose blue-green release gives you a safer way to ship a new container build on a production VPS. You keep two app stacks side by side, test the new one on a second port, then move traffic only after the health check passes. That matters for agency launches, client sites, and support teams that need a clean rollback path if a release breaks login, checkout, or webhook delivery.
This tutorial targets RHEL 9 and compatible systems such as AlmaLinux 9 and Rocky Linux 9. It uses Docker Engine, Docker Compose, SELinux, and firewalld. If you are sizing the host first, Hostperl VPS plans at Hostperl VPS are a practical fit for small production services that need predictable CPU, RAM, and NVMe storage.
We will deploy a simple web app on port 8000 behind a reverse-proxy switchable front end on ports 8080 and 8081. The release flow stays deliberate: prepare the host, create a non-root deployer, enable firewall rules, launch blue and green stacks, test green privately, switch the active port, and keep the old stack ready for rollback.
1) Connect to the VPS and identify the OS
On your local computer, open your first SSH session with the documented example IP below. Replace 203.0.113.10 with your own server IP from Hostperl.
ssh root@203.0.113.10That address is reserved for documentation only. Keep this root session open until the new sudo user is verified.
On the VPS as root, confirm the operating system before you install anything.
cat /etc/os-releaseYou should see rhel, almalinux, or rocky in the output. If the host is not in that family, stop and use a compatible guide.
2) Create the deploy user and prepare SSH access
Use a non-root administrator for day-to-day release work. On RHEL-family systems, sudo access usually goes through the wheel group.
On the VPS as root, create the deploy account, set its password, and add it to wheel.
useradd -m -s /bin/bash deploy
passwd deploy
usermod -aG wheel deploy
id deploySuccess looks like a home directory at /home/deploy and a group list that includes wheel.
Now copy your SSH key. If you do not already have one on your local computer, create it there first with ssh-keygen -t ed25519. Then install the public key on the server.
On the VPS as root, create the SSH directory and set strict permissions.
install -d -m 700 -o deploy -g deploy /home/deploy/.ssh
cat >> /home/deploy/.ssh/authorized_keys <<'EOF'
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIEexampleKeyReplaceWithYourPublicKey
EOF
chown deploy:deploy /home/deploy/.ssh/authorized_keys
chmod 600 /home/deploy/.ssh/authorized_keysReplace the sample key with your real public key. Keep permissions tight; OpenSSH rejects weak ownership or mode settings.
On your local computer, open a second terminal and test the new account before changing root access.
ssh deploy@203.0.113.10Then test sudo.
sudo -iu root idIf that returns uid=0(root), the account is ready. Only after this works should you consider disabling root password login in /etc/ssh/sshd_config.
3) Update the host and install Docker on RHEL 9
On the VPS as the non-root sudo user, update the system and install the packages needed for a production Compose release.
sudo dnf -y update
sudo dnf -y install dnf-plugins-core git firewalld policycoreutils-python-utils
sudo dnf config-manager --add-repo https://download.docker.com/linux/centos/docker-ce.repo
sudo dnf -y install docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-pluginRHEL 9-compatible hosts use the Docker CE repository shown above. The docker-compose-plugin package provides the docker compose command.
Check the installed version.
docker --version
docker compose versionExpected output shows a current Docker Engine and Compose plugin release. If the command is missing, the repository step did not complete.
4) Start Docker, enable firewalld, and open only what you need
On the VPS as the non-root sudo user, start the services and make them persistent across reboots.
sudo systemctl enable --now docker
sudo systemctl enable --now firewalld
sudo systemctl status docker --no-pager
sudo systemctl status firewalld --no-pagerFor a blue-green release, open SSH and the two temporary application ports. Keep them narrow. Do not expose random container ports directly.
sudo firewall-cmd --permanent --add-service=ssh
sudo firewall-cmd --permanent --add-port=8080/tcp
sudo firewall-cmd --permanent --add-port=8081/tcp
sudo firewall-cmd --reload
sudo firewall-cmd --list-allAt this stage, 8080 and 8081 should be reachable only for testing. If you later move traffic behind a reverse proxy, you can remove one of those ports after the new release is proven.
This pattern pairs well with release processes discussed in Docker Compose release readiness for VPS launches and with rollback discipline from Docker Compose deployment rollback on Debian 12 VPS.
5) Build the blue-green project layout
We will keep two Compose files and one shared environment file. The app itself is a small Nginx container for demonstration, but the release method works the same for Node.js, Python, PHP, or Java containers.
On the VPS as the non-root sudo user, create the application directory structure.
sudo mkdir -p /opt/myapp/{blue,green}
sudo chown -R deploy:deploy /opt/myapp
cd /opt/myapp
pwdCreate a shared environment file. Use restrictive permissions because it can hold secrets in a real deployment.
cat > /opt/myapp/.env <<'EOF'
APP_NAME=myapp
APP_PORT=8000
EOF
chmod 600 /opt/myapp/.envNow create the blue stack.
cat > /opt/myapp/blue/compose.yml <<'EOF'
services:
web:
image: nginx:1.27-alpine
container_name: myapp-blue
ports:
- "8080:80"
restart: unless-stopped
EOFCreate the green stack with a different host port.
cat > /opt/myapp/green/compose.yml <<'EOF'
services:
web:
image: nginx:1.27-alpine
container_name: myapp-green
ports:
- "8081:80"
restart: unless-stopped
EOFValidate both files before you start the containers.
cd /opt/myapp/blue && docker compose config
cd /opt/myapp/green && docker compose configIf Compose prints merged YAML without errors, the syntax is acceptable.
6) Launch blue, then build and test green
On the VPS as the non-root sudo user, start the current live stack first.
cd /opt/myapp/blue
docker compose up -dCheck that the blue container is running and listening on port 8080.
docker ps
curl -I http://127.0.0.1:8080You should see an HTTP 200 response from Nginx. If blue is healthy, build the green candidate next.
cd /opt/myapp/green
docker compose up -dTest green without touching the live path.
docker ps
curl -I http://127.0.0.1:8081If green fails here, stop and fix it before any traffic switch. The blue stack is still serving on 8080, so your rollback path remains intact.
7) Use SELinux safely with container ports
RHEL-family systems often enforce SELinux, and container port binding can fail if you ignore it. Confirm the current mode first.
getenforceIf SELinux is enforcing and your containers cannot bind the ports, label the host ports for container use.
sudo semanage port -a -t http_port_t -p tcp 8080
sudo semanage port -a -t http_port_t -p tcp 8081
sudo semanage port -l | grep http_port_tIf the ports already exist, semanage will report that. In that case, do not add them again.
8) Switch traffic to green and keep rollback ready
For a simple service, switching can be as small as changing which host port your proxy or upstream points to. If you are using a reverse proxy, point it at 8081 after the green checks pass. If you are testing directly, verify that your public health check now matches the green stack.
On the VPS as the non-root sudo user, confirm the live candidate and keep blue stopped but present for fallback.
curl -I http://127.0.0.1:8081
sudo docker compose -f /opt/myapp/blue/compose.yml ps
sudo docker compose -f /opt/myapp/green/compose.yml psIf green becomes the active release and blue is no longer needed, stop blue only after you are satisfied with the cutover.
cd /opt/myapp/blue
docker compose downTo roll back, bring blue back up and point traffic to 8080 again.
cd /opt/myapp/blue
docker compose up -d
curl -I http://127.0.0.1:8080That is the core of the rollback: you do not rebuild the old stack under pressure. You re-enable the known-good one.
9) Reboot persistence and post-release checks
Production releases should survive a reboot. Confirm that Docker starts automatically and that both stacks are configured the way you expect.
sudo systemctl is-enabled docker
sudo systemctl is-enabled firewalld
sudo docker ps --format 'table {{.Names}}\t{{.Status}}\t{{.Ports}}'If you use a real application container, add a healthcheck to the Compose file and test it with a real endpoint. For this Nginx example, the smoke test is a simple HTTP header check. For a database-backed app, test the login, write a record, and confirm that the record survives a restart.
For support teams, this release method pairs well with the operational habits in Linux maintenance checklist for VPS in 2026 and with the log-review patterns described in Nginx log files that matter for hosting support in 2026.
If you want a VPS that can handle container releases without constant resizing, Hostperl can supply the right balance of CPU, RAM, and NVMe storage for small production apps. For launch-ready hosting, Hostperl VPS hosting gives you room to keep blue and green stacks online during cutovers, while dedicated server hosting suits heavier services and higher-traffic windows.
Our support team can also help you plan migration windows, open the right firewall rules, and confirm that the old stack stays available until the new one is proven.
10) Troubleshooting the most likely failures
Problem: Docker will not start. Diagnose it with:
sudo systemctl status docker --no-pager
sudo journalctl -u docker -b --no-pager | tail -n 50Look for repository errors, storage driver issues, or permission problems. Reinstall the package or remove a broken daemon config only after you understand the log output.
Problem: Port 8080 or 8081 is unreachable. Check the listener and firewall rules.
sudo ss -lntp | grep ':8080\|:8081'
sudo firewall-cmd --list-portsIf the port is missing from firewalld, add it again and reload. If the container is not listening, inspect the Compose output and container logs.
cd /opt/myapp/green
docker compose logs --tail=50Problem: SELinux blocks the container bind. Confirm the denial clues in the audit log.
sudo ausearch -m AVC -ts recent | tail -n 20Then label the port with semanage port as shown earlier. Do not disable SELinux globally for a routine container release.
Problem: SSH user cannot sudo. Verify group membership.
id deploy
sudo -l -U deployIf wheel is missing, run usermod -aG wheel deploy from root, then open a new SSH session because group membership changes do not always apply to existing logins.
FAQ
Can I use this workflow on AlmaLinux 9 or Rocky Linux 9? Yes. The commands work on the RHEL 9 family, including SELinux and firewalld.
Why keep both blue and green online? Because it gives you a fast rollback. If the new release breaks authentication, checkout, or an API callback, you can point traffic back immediately.
Do I need a load balancer? Not for this pattern. A single VPS can switch between two local Compose stacks. A load balancer becomes useful when you need true zero-downtime traffic shifting across multiple hosts.
Should I expose both app ports permanently? No. Use them for release validation, then close the unused port after cutover if your architecture no longer needs it.
What is the safest next step after this tutorial? Add a healthcheck, wire the switch behind Nginx or another proxy, and document the rollback command so support staff can execute it under pressure.
