IPv4 & IPv6 Leasing - Any RIR, Any LocationOrder Now
Hostperl

Blue-Green Docker Deployments on RHEL 9 VPS

By Raman Kumar

Share:

Updated on Sep 3, 2026

Blue-Green Docker Deployments on RHEL 9 VPS

What this deployment pattern solves

Blue-Green Docker deployments let you launch a new app version on a second stack, test it on the live server, and move traffic only after health checks pass. On a RHEL 9 VPS, that matters for customer-facing apps, agency cutovers, and any release window where downtime is not an option.

This tutorial is written for a fresh RHEL 9, Oracle Linux 9, or compatible RHEL-family server. If you want a managed VPS built for this kind of release workflow, start with Hostperl VPS. For release planning context, pair this guide with Docker Compose release readiness for VPS launches.

You will create a non-root admin user, install Docker Compose, run two app instances, place Nginx in front as the traffic switch, and keep rollback straightforward. The sample app listens on port 8000 inside the container, while Nginx exposes 80 and 443 to the internet.

Architecture and release flow

We will use two application directories: /opt/myapp/blue and /opt/myapp/green. Only one serves production traffic at a time. Nginx proxies to the active stack on localhost, so the cutover happens by changing one upstream file and reloading Nginx.

  • Blue: current production stack
  • Green: new version under test
  • Nginx: public entry point and cutover layer
  • Systemd: starts Docker on boot
  • Firewalld: opens only the ports you need

If you want a quick check on platform fit and sizing before release work starts, Hostperl’s general hosting guidance is easier to apply operationally than generic developer advice. A second useful reference is Docker deployment checklist for safer Hostperl launches.

1) Connect to the server and confirm the OS

On your local computer

ssh root@203.0.113.10

Replace 203.0.113.10 with the public IP assigned to your server. This documentation IP is only an example.

If your provider gave you a default non-root account, use that instead and then elevate with sudo:

ssh deploy@203.0.113.10

On the VPS as root

cat /etc/os-release

Confirm you are on RHEL 9, Oracle Linux 9, or a close RHEL-compatible system. The commands below assume a RHEL-family base.

2) Create the deploy user and lock down SSH access

On the VPS as root

useradd -m -G wheel -s /bin/bash deploy

This creates the admin account. Set a password now so you still have a local recovery path if SSH keys fail:

passwd deploy

Install sudo access through the wheel group and prepare the SSH directory:

mkdir -p /home/deploy/.ssh
chmod 700 /home/deploy/.ssh
chown -R deploy:deploy /home/deploy/.ssh

Add your public key. Replace the key text with your own key from your local computer:

cat > /home/deploy/.ssh/authorized_keys <<'EOF'
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIM7examplekeyvaluehere user@laptop
EOF
chmod 600 /home/deploy/.ssh/authorized_keys
chown deploy:deploy /home/deploy/.ssh/authorized_keys

Keep the root session open. Open a second terminal and test login before changing any SSH policy.

On your local computer

ssh deploy@203.0.113.10

Then confirm sudo works:

sudo -v
whoami

You should see deploy for the username and no sudo error. Only after this succeeds should you harden SSH further.

3) Update packages, time sync, and Docker

On the VPS as the non-root sudo user

sudo dnf -y update

Install Docker, Compose, Nginx, and utilities:

sudo dnf -y install dnf-plugins-core docker nginx firewalld curl git rsync policycoreutils-python-utils

Enable and start the services:

sudo systemctl enable --now docker nginx firewalld chronyd

Check versions so you know the installation actually landed:

docker --version
docker compose version
nginx -v
systemctl status docker --no-pager

On RHEL-family systems, SELinux is usually enforcing. Leave it that way. We will label the files correctly instead of turning enforcement off.

4) Open the firewall safely

On the VPS as the non-root sudo user

sudo firewall-cmd --permanent --add-service=ssh
sudo firewall-cmd --permanent --add-service=http
sudo firewall-cmd --permanent --add-service=https
sudo firewall-cmd --reload
sudo firewall-cmd --list-all

This adds the new rules before you touch anything else. You should see SSH, HTTP, and HTTPS in the active zone. Do not remove your current access method until the new one works.

5) Build the blue-green app layout

On the VPS as the non-root sudo user

sudo mkdir -p /opt/myapp/{blue,green}
sudo chown -R deploy:deploy /opt/myapp
cd /opt/myapp
tree -L 2

Create a small test app image that serves a release label. That keeps the deployment process repeatable and makes cutover checks obvious.

On the VPS as the non-root sudo user

cd /opt/myapp
cat > Dockerfile <<'EOF'
FROM nginx:1.27-alpine
RUN printf 'server {\n  listen 8000;\n  location / {\n    add_header Content-Type text/plain;\n    return 200 "release=$RELEASE\n";\n  }\n}\n' > /etc/nginx/conf.d/default.conf
CMD ["nginx", "-g", "daemon off;"]
EOF

Create the first compose file for blue:

On the VPS as the non-root sudo user

cat > /opt/myapp/blue/compose.yml <<'EOF'
services:
  app:
    build:
      context: ..
    environment:
      RELEASE: blue
    ports:
      - "127.0.0.1:18080:8000"
EOF

Create the green variant:

On the VPS as the non-root sudo user

cat > /opt/myapp/green/compose.yml <<'EOF'
services:
  app:
    build:
      context: ..
    environment:
      RELEASE: green
    ports:
      - "127.0.0.1:28080:8000"
EOF

Bring up blue first:

On the VPS as the non-root sudo user

cd /opt/myapp/blue
sudo docker compose -f compose.yml up -d --build
sudo docker ps
curl http://127.0.0.1:18080

You should receive release=blue. That confirms the production stack is alive on localhost before Nginx gets involved.

6) Configure Nginx as the traffic switch

Create a simple upstream file that points Nginx at the active stack. Start with blue.

On the VPS as the non-root sudo user

sudo tee /etc/nginx/conf.d/myapp-upstream.conf </dev/null <<'EOF'
upstream myapp_active {
    server 127.0.0.1:18080;
}
EOF

Create the site config:

On the VPS as the non-root sudo user

sudo tee /etc/nginx/conf.d/myapp.conf </dev/null <<'EOF'
server {
    listen 80;
    server_name server.example.com;

    location /healthz {
        proxy_pass http://myapp_active;
        proxy_set_header Host $host;
    }

    location / {
        proxy_pass http://myapp_active;
        proxy_set_header Host $host;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }
}
EOF

Test syntax before reloading:

On the VPS as the non-root sudo user

sudo nginx -t
sudo systemctl reload nginx

Verify from the server:

On the VPS as the non-root sudo user

curl -i http://127.0.0.1/healthz

You should see HTTP 200 and release=blue. If SELinux blocks proxying, inspect the logs next.

7) Pre-stage green and compare it before cutover

On the VPS as the non-root sudo user

cd /opt/myapp/green
sudo docker compose -f compose.yml up -d --build
sudo docker ps
curl http://127.0.0.1:28080

That output should show release=green. Now you can test the new release privately without touching public traffic.

For database-backed apps, this is the point where you would validate schema changes or migration scripts. If your deployment also needs PostgreSQL backup planning, Hostperl’s recovery notes are a useful companion: PostgreSQL backups and restores for VPS recovery.

8) Cut traffic from blue to green

Swap the upstream file to point at the green container, then reload Nginx. This is the actual blue-green switch.

On the VPS as the non-root sudo user

sudo tee /etc/nginx/conf.d/myapp-upstream.conf </dev/null <<'EOF'
upstream myapp_active {
    server 127.0.0.1:28080;
}
EOF
sudo nginx -t
sudo systemctl reload nginx

Check the live endpoint:

On the VPS as the non-root sudo user

curl -i http://127.0.0.1/healthz

You should now see release=green. From a second terminal on your local computer, test the public IP or DNS name:

On your local computer

curl -i http://203.0.113.10/healthz

Replace the example IP with your real server address. The response should match the green release.

9) Add HTTPS with Let’s Encrypt

If your domain already points to the server, install Certbot and issue a certificate. On RHEL 9, use the EPEL package first.

On the VPS as the non-root sudo user

sudo dnf -y install epel-release
sudo dnf -y install certbot python3-certbot-nginx

Request the certificate. Replace server.example.com and example.com with your real host and domain names:

On the VPS as the non-root sudo user

sudo certbot --nginx -d server.example.com

Certbot should update the Nginx config and install renewal files. Confirm the timer exists:

On the VPS as the non-root sudo user

systemctl list-timers | grep certbot
sudo certbot renew --dry-run

For DNS, TLS, and mail-delivery follow-on work, Hostperl also keeps a practical DNS and email path documented in DNSSEC and email authentication on Ubuntu Server 24.04. The OS differs, but the validation habits carry over well.

10) Make the deployment survive a reboot

Docker and Nginx are already enabled, but you should confirm persistence by rebooting once during a maintenance window. That catches missing boot dependencies before a customer does.

On the VPS as the non-root sudo user

sudo systemctl is-enabled docker nginx firewalld
sudo reboot

Reconnect after the server comes back:

On your local computer

ssh deploy@203.0.113.10

Then verify the active release and service state:

On the VPS as the non-root sudo user

systemctl status docker nginx firewalld --no-pager
sudo docker ps
curl -i http://127.0.0.1/healthz

Rollback and recovery

If green fails after cutover, switch the upstream back to blue and reload Nginx. That gives you a fast rollback without rebuilding images.

On the VPS as the non-root sudo user

sudo tee /etc/nginx/conf.d/myapp-upstream.conf </dev/null <<'EOF'
upstream myapp_active {
    server 127.0.0.1:18080;
}
EOF
sudo nginx -t
sudo systemctl reload nginx

If both releases are broken, keep Nginx pointed at the last known-good container and inspect logs:

On the VPS as the non-root sudo user

sudo journalctl -u nginx -n 50 --no-pager
sudo journalctl -u docker -n 50 --no-pager
sudo docker logs $(sudo docker ps -q --filter publish=18080)
sudo docker logs $(sudo docker ps -q --filter publish=28080)

For broader release recovery practice, review WordPress recovery plan for updates, migrations, and rollbacks. The application differs, but the rollback discipline is the same.

Troubleshooting the most common failures

  • Nginx returns 502: run curl http://127.0.0.1:18080 and curl http://127.0.0.1:28080. If one fails, rebuild that stack with sudo docker compose -f compose.yml up -d --build.
  • SELinux blocks proxying: check sudo ausearch -m AVC -ts recent. If you see denial messages, confirm the config stays under /etc/nginx/conf.d and the proxy target is on localhost. Do not disable SELinux to hide the problem.
  • Firewall looks open but the site is unreachable: run sudo firewall-cmd --list-all and ss -tulpn | grep -E ':80|:443'. If Nginx is not listening, check sudo systemctl status nginx.
  • Compose does not start after reboot: inspect sudo journalctl -u docker -b --no-pager and confirm Docker is enabled with systemctl is-enabled docker.

Final verification checklist

Finish with one server-side and one client-side smoke test.

On the VPS as the non-root sudo user

sudo nginx -t
sudo docker ps
curl -i http://127.0.0.1/healthz
ss -tulpn | grep -E ':80|:443'

On your local computer

curl -i http://203.0.113.10/healthz

If you want a more capacity-oriented VPS for release testing, active deployments, and customer workloads, Hostperl’s managed VPS hosting is a better fit than pushing production releases onto under-sized shared resources. That matters when your cutover window is measured in minutes and support response matters.

Hostperl is a practical choice if you want to run Blue-Green Docker deployments without guessing about uptime, firewall policy, or recovery steps. Our VPS hosting gives you the control needed for Docker, Nginx, and clean release cutovers, while our support team can help you plan migrations and rollback windows.

If your app serves customers in NZ or APAC, that extra operational margin matters on release day.

FAQ

Can I use this pattern on Ubuntu or Debian?
Yes. The release flow is the same, but package names, firewall tooling, and SELinux handling change. On Debian-family systems you would use apt and usually UFW instead of firewalld.

Why proxy through Nginx instead of exposing the container port?
Nginx gives you one stable public endpoint, simpler TLS renewal, access logs, and a clean place to switch between blue and green.

What if my app needs a database migration?
Run the migration against the green stack before cutover, then verify it with live reads and writes. If the migration fails, keep blue active and fix the new schema first.

Is this safe for production?
Yes, if you keep blue intact, validate green before switching, test syntax before reloads, and keep rollback commands ready. That is the point of the pattern.

Should I disable root SSH after this?
Only after you have confirmed the deploy user can reconnect from a second terminal and run sudo. Keep a fallback console path if your provider offers one.