What you will build
This tutorial walks you through a Docker Compose deployment on a fresh dedicated server, from the first SSH login to a working web app behind Nginx. You will create a non-root admin user, install Docker and Docker Compose, run a sample application, add a reverse proxy, open only the ports you need, and check that the stack survives a reboot. The commands are written for a new HostnExtra dedicated server and split for Ubuntu/Debian and AlmaLinux/Rocky Linux where the steps differ.
Recommended reading before you start: How to Choose a Dedicated Server in 2026, How to Compare Dedicated Server Options in 2026, and HostnExtra dedicated servers.
Focus keyword: Docker Compose deployment.
Prerequisites and first SSH login
- A fresh dedicated server with Ubuntu 26.04 LTS, Debian, AlmaLinux 10, or Rocky Linux 10.
- At least 2 CPU cores, 2 GB RAM, and 20 GB free disk for the sample app.
- A domain such as
app.example.compointing to your server IP. - Ports 22, 80, and 443 available on the server and in any upstream firewall.
- Local SSH key pair on your computer.
Local computer: open a terminal and connect as root. Replace the placeholder values with your own.
ssh [email protected]If your server uses a custom SSH port, connect with the port explicitly so you do not assume the default.
ssh -p 2222 [email protected]After login, check the operating system before choosing package commands.
cat /etc/os-releaseLook for ID and VERSION_ID. Keep this root session open until the new sudo user works as expected.
Create a sudo user and move to key-based access
VPS as root: create the admin account, set a password, add it to the sudo or wheel group, and prepare SSH access. Replace ADMIN_USER with your own name.
ADMIN_USER=deploy
adduser "$ADMIN_USER"
passwd "$ADMIN_USER"The first command creates the account. The second sets its password. You should see no errors. Next, grant administrative access:
# Ubuntu / Debian
usermod -aG sudo "$ADMIN_USER"
# AlmaLinux / Rocky Linux
usermod -aG wheel "$ADMIN_USER"Now create the SSH directory and copy your public key. Replace the key path if needed.
install -d -m 700 -o "$ADMIN_USER" -g "$ADMIN_USER" /home/$ADMIN_USER/.ssh
cat /home/$ADMIN_USER/.ssh/authorized_keysIf you already have a public key on your local computer, append it through SSH or paste it carefully into authorized_keys. Then fix ownership and permissions.
chown -R "$ADMIN_USER:$ADMIN_USER" /home/$ADMIN_USER/.ssh
chmod 700 /home/$ADMIN_USER/.ssh
chmod 600 /home/$ADMIN_USER/.ssh/authorized_keysLocal computer: open a second terminal and test the new user before changing root access.
ssh [email protected]Then test sudo in that second session.
sudo -vExpected result: the command should ask for the user password or use cached sudo and return cleanly. If it fails, keep the original root session open and fix the account first.
Update the server and set the hostname
VPS as the sudo user: update packages and set a useful hostname for logs and prompts.
# Ubuntu / Debian
sudo apt update
sudo apt full-upgrade -y
sudo apt install -y curl ca-certificates gnupg lsb-release ufw
# AlmaLinux / Rocky Linux
sudo dnf -y upgrade
sudo dnf -y install curl ca-certificates gnupg2 firewalldThese commands refresh the package index and install the tools you will use later. When they finish cleanly, there should be no dependency errors.
sudo hostnamectl set-hostname app.example.com
hostnamectlThe second command confirms the new hostname. If you want time sync, verify that systemd timesync is active:
timedatectl statusLook for synchronized time and the correct time zone.
Install Docker and Docker Compose
VPS as the sudo user: install Docker Engine and the Compose plugin using the supported repository path for your distribution. Do not mix package families.
Ubuntu and Debian
sudo install -m 0755 -d /etc/apt/keyrings
curl -fsSL https://download.docker.com/linux/$(. /etc/os-release; echo "$ID")/gpg | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg
sudo chmod a+r /etc/apt/keyrings/docker.gpg
echo \
"deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/$(. /etc/os-release; echo "$ID") \
$(. /etc/os-release; echo "$VERSION_CODENAME") stable" | \
sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
sudo apt update
sudo apt install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
sudo systemctl enable --now docker
sudo docker --version
sudo docker compose versionAlmaLinux and Rocky Linux
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-plugin
sudo systemctl enable --now docker
sudo docker --version
sudo docker compose versionIf you want the admin user to run Docker without typing sudo every time, add it to the docker group and open a new shell.
sudo usermod -aG docker "$ADMIN_USER"
newgrp dockerUse this only after you understand the access change; Docker group membership is effectively privileged.
Create the application and Compose files
We will deploy a small Nginx container first. It gives you a clean smoke test for the whole Docker Compose deployment.
VPS as the sudo user: create the app directory and an HTML file.
APP_DIR=/opt/example-app
sudo mkdir -p "$APP_DIR"/html
sudo chown -R "$ADMIN_USER:$ADMIN_USER" "$APP_DIR"Create the web content file with your editor.
cat > "$APP_DIR"/html/index.html <<'EOF'
<!doctype html>
<html lang="en">
<head><meta charset="utf-8"><title>Docker Compose on HostnExtra</title></head>
<body><h1>It works</h1><p>Docker Compose deployment is live.</p></body>
</html>
EOFNow create the Compose file.
cat > "$APP_DIR"/compose.yml <<'EOF'
services:
web:
image: nginx:alpine
container_name: example-web
ports:
- "8080:80"
volumes:
- ./html:/usr/share/nginx/html:ro
restart: unless-stopped
EOFValidate the file syntax before starting the stack.
cd /opt/example-app
sudo docker compose configExpected result: Docker prints the resolved configuration and exits without syntax errors.
Start the container and test it locally
VPS as the sudo user: start the service and inspect its status.
sudo docker compose up -d
sudo docker ps
sudo docker logs example-web --tail 20The container should show as running. The logs should not contain startup errors.
Test the app from the server itself.
curl -I http://127.0.0.1:8080
curl http://127.0.0.1:8080You should see HTTP headers and the sample HTML page. If the port does not answer, recheck the Compose file and container logs.
Open the firewall safely
VPS as the sudo user: open only the required ports. Replace the firewall section with the one that matches your operating system.
Ubuntu and Debian with UFW
sudo ufw allow OpenSSH
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw enable
sudo ufw status verboseAlmaLinux and Rocky Linux with firewalld
sudo systemctl enable --now firewalld
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-allAt this stage, only port 8080 is used internally by the container, while Nginx will later listen on 80 and 443.
Put Nginx in front of the container
Use Nginx on the host to handle the public web ports and forward traffic to the container. That keeps the setup simple and easier to maintain.
VPS as the sudo user: install Nginx.
# Ubuntu / Debian
sudo apt install -y nginx
# AlmaLinux / Rocky Linux
sudo dnf -y install nginxCreate the reverse proxy configuration.
# Ubuntu / Debian
sudo tee /etc/nginx/sites-available/example-app > /dev/null <<'EOF'
server {
listen 80;
server_name app.example.com;
location / {
proxy_pass http://127.0.0.1:8080;
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;
}
}
EOF
sudo ln -s /etc/nginx/sites-available/example-app /etc/nginx/sites-enabled/example-app
sudo rm -f /etc/nginx/sites-enabled/default
# AlmaLinux / Rocky Linux
sudo tee /etc/nginx/conf.d/example-app.conf > /dev/null <<'EOF'
server {
listen 80;
server_name app.example.com;
location / {
proxy_pass http://127.0.0.1:8080;
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;
}
}
EOFTest the Nginx syntax before reloading it.
sudo nginx -tExpected result: syntax is ok and test is successful. Then start and reload Nginx.
sudo systemctl enable --now nginx
sudo systemctl reload nginxConfirm the site works on the server loopback.
curl -I http://127.0.0.1
curl http://127.0.0.1Add TLS with Let’s Encrypt
Once DNS points to the server, issue a certificate for the public name. Replace the email address with your own.
Ubuntu and Debian
sudo apt install -y certbot python3-certbot-nginx
sudo certbot --nginx -d app.example.com -m [email protected] --agree-tos --redirect --non-interactive
sudo certbot renew --dry-runAlmaLinux and Rocky Linux
sudo dnf -y install certbot python3-certbot-nginx
sudo certbot --nginx -d app.example.com -m [email protected] --agree-tos --redirect --non-interactive
sudo certbot renew --dry-runAfter issuance, test HTTPS from the server and your local machine.
curl -I https://app.example.com
curl https://app.example.comPersistence checks and rollback
VPS as the sudo user: confirm the stack returns after reboot.
sudo rebootReconnect from your local computer after the server returns.
ssh [email protected]Then verify the services and container again.
sudo systemctl status docker --no-pager
sudo systemctl status nginx --no-pager
sudo docker ps
curl -I https://app.example.comIf you need to roll back, stop the app first, then remove the files in reverse order.
cd /opt/example-app
sudo docker compose down
sudo rm -f /etc/nginx/sites-enabled/example-app /etc/nginx/sites-available/example-app
sudo rm -f /etc/nginx/conf.d/example-app.conf
sudo systemctl reload nginx
sudo docker system prune -fThis removes the sample deployment without touching the OS or your SSH access.
Troubleshooting
- Container not running: run
sudo docker ps -aandsudo docker logs example-web. Look for image pull or volume errors. Fix the Compose file and runsudo docker compose up -dagain. - Nginx 502 Bad Gateway: run
curl -I http://127.0.0.1:8080. If it fails, the container is not serving. Restart it withsudo docker compose restart web. - Let’s Encrypt challenge fails: run
sudo nginx -tandsudo ss -ltnp | grep ':80'. Ensure DNS points to the right IP and no other service is using port 80. - Firewall blocks access: run
sudo ufw status verboseorsudo firewall-cmd --list-all. Re-add ports 80 and 443 if needed, then retest. - Docker does not start after reboot: run
sudo systemctl status docker. If disabled, runsudo systemctl enable --now docker.
Why this approach works for fresh dedicated servers
This pattern keeps the public surface small, uses a repeatable Compose file, and gives you clear rollback steps. It fits launch sites, internal tools, and customer projects that need predictable operations instead of platform complexity. If you are planning the underlying hardware, HostnExtra dedicated servers are a good fit for this style of deployment: dedicated infrastructure, with regional options including the United States, Germany, and Netherlands.
Need a fresh server for a Docker Compose deployment? Start with a dedicated machine, a public IP, and a clean OS install. That gives you the simplest path to repeatable application hosting.
FAQ
Can I use this on Ubuntu and AlmaLinux alike? Yes, but package commands, firewall tools, and repository setup differ. Follow the section that matches your OS after running cat /etc/os-release.
Should I expose the container port to the internet? No. Keep the app on localhost and publish only Nginx on 80 and 443. That makes TLS and traffic filtering easier.
What should I check first if the site is down after reboot? Check sudo systemctl status docker, sudo systemctl status nginx, and sudo docker ps. Those commands show whether the host services and container started correctly.

