Docker Compose Rollouts on AlmaLinux 9 Without Downtime

Why this rollout pattern works on AlmaLinux 9
If you need Docker Compose rollouts on AlmaLinux 9, the safest pattern is simple: keep the old stack live until the new one clears a local health check, a reverse-proxy syntax test, and a client-side smoke test. That gives you a real rollback path if a container fails to start, a port is wrong, or SELinux blocks access.
This tutorial uses AlmaLinux 9 because it matches a common Hostperl production setup for teams that want RHEL-compatible operations, firewalld, and SELinux in place. If you are planning a new app host on a Hostperl VPS, this approach keeps releases predictable for agencies, small businesses, and support teams that cannot afford avoidable cutovers.
We will deploy a simple application behind Nginx, run it with Docker Compose, protect the host with firewalld and SELinux, and finish with a rollback test. For related operational context, see Docker Compose health checks for safer app releases and Nginx reverse proxy logs that speed up app support.
What you will build
You will set up:
- AlmaLinux 9 host verification and package updates
- a non-root
deployaccount with sudo access - Docker Engine and Docker Compose plugin
- a small containerized web app with a pinned image
- Nginx as the public reverse proxy
- firewalld rules for SSH, HTTP, and HTTPS
- SELinux-friendly file locations and labels
- verification, rollback, and troubleshooting steps
This is not a generic Docker introduction. The goal is a production release process you can repeat for client sites, internal apps, and staged launches.
1) Connect to the server and identify the OS
On your local computer, open your first SSH session to the server:
ssh root@203.0.113.10Replace 203.0.113.10 with the real public IP assigned to your server. Keep this root session open while you create and test the non-root administrator account.
On the VPS as root, confirm the operating system:
cat /etc/os-releaseYou should see AlmaLinux 9 details. If you are on a different OS, stop here and use the AlmaLinux/Rocky-compatible branch only when it is actually supported.
2) Update the host and prepare a sudo user
On the VPS as root, update the system first. This reduces package drift before you add runtime dependencies.
dnf -y updateNext, create the non-root admin account. The deploy user is the example name used throughout this guide.
useradd -m -G wheel deploy
passwd deployThe first command creates the home directory and adds the user to the wheel group. The second command sets a password so you can test a clean login before moving to SSH keys only.
Now install sudo if it is missing and verify the wheel group has sudo access.
dnf -y install sudo
visudoIn the editor, make sure this line is present and not commented out:
%wheel ALL=(ALL) ALLSave and exit the editor. On AlmaLinux 9, this is usually already enabled, but checking it now avoids a lockout later.
3) Add SSH keys safely before changing access
Keep the root session open. Open a second terminal on your local computer and create a new login for deploy.
On your local computer, if you already have an SSH key, copy it to the server. Replace 203.0.113.10 with your real server IP.
ssh-copy-id deploy@203.0.113.10If you need a new key pair first, run:
ssh-keygen -t ed25519 -C "deploy@server.example.com"Follow the prompts and keep the default path unless you already manage multiple keys.
On the VPS as root, tighten the SSH directory permissions if you install keys manually.
mkdir -p /home/deploy/.ssh
cp /root/.ssh/authorized_keys /home/deploy/.ssh/authorized_keys
chown -R deploy:deploy /home/deploy/.ssh
chmod 700 /home/deploy/.ssh
chmod 600 /home/deploy/.ssh/authorized_keysThose permissions matter. SSH will ignore the key file if ownership or modes are too open.
4) Test the non-root login before any hardening
On your local computer, open a second SSH session:
ssh deploy@203.0.113.10Once logged in, confirm sudo works:
sudo -v
whoami
pwdYou should see your password prompt for sudo, whoami should return deploy, and pwd should show your home directory. Do not disable root login until this works cleanly.
5) Install Docker Engine and the Compose plugin
On the VPS as the non-root sudo user, add Docker’s repository and install the current packages supported by AlmaLinux 9.
sudo dnf -y install dnf-plugins-core
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-pluginStart Docker and enable it at boot:
sudo systemctl enable --now docker
sudo systemctl status docker --no-pagerCheck the installed Compose version:
docker compose versionYou should see the Docker Compose plugin version, not a missing-command error. If Docker fails to start, inspect the journal before continuing:
sudo journalctl -u docker -b --no-pager | tail -n 506) Create the application directory and Compose files
We will use /opt/myapp as the app directory. This keeps the deployment separate from user homes and makes ownership easier to audit.
On the VPS as root, create the directories and hand ownership to deploy:
mkdir -p /opt/myapp/{app,nginx}
chown -R deploy:deploy /opt/myappOn the VPS as the non-root sudo user, create the application file. This example uses a tiny Python app so you can test the rollout process without depending on external build steps.
cd /opt/myapp/app
cat > app.py <<'EOF'
from http.server import BaseHTTPRequestHandler, HTTPServer
class Handler(BaseHTTPRequestHandler):
def do_GET(self):
body = b'OK: Docker Compose rollout on AlmaLinux 9\n'
self.send_response(200)
self.send_header('Content-Type', 'text/plain; charset=utf-8')
self.send_header('Content-Length', str(len(body)))
self.end_headers()
self.wfile.write(body)
HTTPServer(('0.0.0.0', 8000), Handler).serve_forever()
EOF
cat > Dockerfile <<'EOF'
FROM python:3.12-alpine
WORKDIR /app
COPY app.py /app/app.py
EXPOSE 8000
CMD ["python", "/app/app.py"]
EOFNow create the Compose file. Pinning the build context keeps the release self-contained.
cd /opt/myapp
cat > compose.yaml <<'EOF'
services:
app:
build: ./app
container_name: myapp-app
restart: unless-stopped
expose:
- "8000"
healthcheck:
test: ["CMD", "python", "-c", "import urllib.request; print(urllib.request.urlopen('http://127.0.0.1:8000').read().decode())"]
interval: 10s
timeout: 3s
retries: 3
start_period: 5s
nginx:
image: nginx:1.27-alpine
container_name: myapp-nginx
restart: unless-stopped
depends_on:
app:
condition: service_healthy
ports:
- "80:80"
volumes:
- ./nginx/default.conf:/etc/nginx/conf.d/default.conf:ro
healthcheck:
test: ["CMD", "wget", "-qO-", "http://127.0.0.1/" ]
interval: 10s
timeout: 3s
retries: 3
start_period: 5s
EOFCreate the Nginx reverse proxy file that sends traffic to the app container.
cd /opt/myapp/nginx
cat > default.conf <<'EOF'
server {
listen 80;
server_name server.example.com;
location / {
proxy_pass http://app:8000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
EOFReplace server.example.com with your real hostname when you are ready for production DNS. The container will still run before DNS is updated.
7) Test the compose syntax and build the stack
On the VPS as the non-root sudo user, validate the Compose file before starting anything:
cd /opt/myapp
docker compose configIf the file is valid, Compose prints the merged configuration. Any indentation or syntax mistake will fail here, which is exactly what you want before a launch.
Now build and start the stack:
docker compose up -d --buildConfirm the containers are running:
docker compose psThen check the app logs and the reverse proxy logs:
docker compose logs --no-color --tail=50 app nginxIf the app container exits, fix the Python file or the Dockerfile before you go further.
8) Open the firewall and account for SELinux
AlmaLinux uses firewalld by default. Open SSH and HTTP before you test the service from a browser.
On the VPS as root, allow the required ports:
firewall-cmd --permanent --add-service=ssh
firewall-cmd --permanent --add-service=http
firewall-cmd --reload
firewall-cmd --list-allYou should see ssh and http in the active zone. Leave HTTPS closed for now unless you add TLS in the next step.
Because this stack mounts one Nginx config file read-only, SELinux normally stays happy. If you later mount application data into containers, check for AVC denials with:
sudo ausearch -m avc -ts recentFor this tutorial, no extra SELinux policy change should be needed. If you see denials, do not disable SELinux globally. Fix the label or access pattern instead.
9) Put Nginx on the host in front of the Compose app
Using host Nginx gives you cleaner logs and a stable place for TLS later. Install it now and point it to the Docker app.
On the VPS as root, install Nginx:
dnf -y install nginx
systemctl enable --now nginxCreate the site file:
cat > /etc/nginx/conf.d/myapp.conf <<'EOF'
server {
listen 80;
server_name server.example.com;
location / {
proxy_pass http://127.0.0.1:80;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
EOFThis example assumes the containerized Nginx is already publishing port 80 on the host. If you prefer to let host Nginx proxy directly to the app container, change the upstream to the Docker bridge IP or add a Compose network alias. For most small production launches, publishing the proxy container is simpler.
Before reloading Nginx, test syntax:
nginx -tIf the test passes, reload the service:
systemctl reload nginx10) Verify the rollout from the server and from your laptop
On the VPS as the non-root sudo user, confirm the service answers locally:
curl -i http://127.0.0.1/You should see HTTP/1.1 200 OK and the body OK: Docker Compose rollout on AlmaLinux 9.
Check the listening ports:
sudo ss -tulpn | grep -E ':80|:22'On your local computer, test the public URL:
curl -I http://203.0.113.10Replace 203.0.113.10 with your server’s real IP. A 200 response or a redirect from your front-end proxy means traffic is reaching the host correctly.
If you have a hostname in DNS, test that too:
curl -i http://server.example.comThe response should match the local test. If it does not, check the DNS A record and the Nginx server_name value.
11) Add a clean rollback path before every release
A practical rollout needs one command set for the new version and one for restoring the previous version. The easiest rollback on this stack is to keep the last working Compose project in a Git repository or a dated backup directory.
On the VPS as the non-root sudo user, create a simple backup of the working release:
cd /opt
sudo cp -a myapp myapp.backup-$(date +%F)If the new release fails, you can stop the stack, restore the previous tree, and start it again:
cd /opt/myapp
docker compose down
cd /opt
sudo rm -rf myapp
sudo cp -a myapp.backup-2026-10-02 myapp
cd /opt/myapp
docker compose up -d --buildReplace the backup directory name with the one created on your server. Keep the old backup until the new version has run through at least one successful smoke test.
12) Troubleshoot the failures you are most likely to see
If the container does not start, check the service state and recent logs:
docker compose ps
docker compose logs --tail=100 app nginxLook for image build errors, bad Python syntax, or missing files in /opt/myapp. Rebuild after fixing the file:
docker compose up -d --buildIf Nginx fails to reload, the syntax test usually tells you why:
nginx -tA missing semicolon or bad upstream address is the usual clue. Correct the config, then reload again.
If the browser cannot reach the site, confirm the host firewall and listening sockets:
firewall-cmd --list-all
sudo ss -tulpn | grep -E ':80|:443|:22'If you see SELinux denials, inspect them directly instead of disabling SELinux:
sudo ausearch -m avc -ts recent | tail -n 20If a denial appears right after you add a volume mount, adjust the path labels or mount type. That keeps the host secure and avoids surprise breakage after reboot.
13) Reboot test and final operational check
A rollout is not finished until it survives a reboot. This catches services that start in the wrong order or dependencies that were never enabled.
On the VPS as root, reboot the server:
rebootAfter the server returns, log in again as deploy and confirm the services are active:
sudo systemctl status docker --no-pager
sudo systemctl status nginx --no-pager
docker compose -f /opt/myapp/compose.yaml psRun one last functional test from the host and from your laptop:
curl -i http://127.0.0.1/
curl -I http://203.0.113.10If both return healthy responses, the deployment is ready for production traffic.
Conclusion
This workflow gives you a repeatable release path for Docker applications on AlmaLinux 9: a verified admin login, working Docker Compose services, a checked Nginx configuration, firewalld rules, and a rollback copy. It fits real hosting operations because it leaves room for support, changes, and recovery instead of assuming every deploy succeeds first time.
If you are moving this kind of app to a larger environment, Hostperl can place it on a managed VPS hosting plan or on a dedicated platform when your traffic, storage, or isolation needs grow. For teams handling more complex releases, dedicated server hosting is the better fit when you need more control over networking, memory, or persistent workloads.
If you want a production home for Docker apps, Hostperl can help you choose the right VPS or dedicated platform, then support the migration and first launch. For most containerized workloads, start with Hostperl VPS; for heavier multi-service deployments, review dedicated server hosting.
FAQ
Should I run Docker Compose as root?
No. Create a sudo user such as deploy, test that login, and keep root only for host-level tasks like firewall changes and package installation.
Do I need host Nginx if the container already exposes port 80?
Not always, but host Nginx gives you a stable place for TLS, log review, and maintenance changes without touching the app container.
What is the safest rollback method?
Keep the last known good /opt/myapp tree, stop the stack, restore the backup, and start the same Compose file again. Test it before deleting the backup.
Why use AlmaLinux 9 for this tutorial?
AlmaLinux 9 gives you RHEL-compatible package management, firewalld, and SELinux, which are common on production hosting servers in 2026.
