Deploying Docker production on Contabo
Introduction

I’ll take you through how I deployed a Docker application inside a Contabo VPS.
deployment
This blog is mainly aboutdeployment. I won’t go through setting up Docker locally or building the application. I’m assuming you already have those parts ready.
The goal is simple:
Deploy my application to the Contabo instance.
Connect Cloudflare and handle the domain/DNS side.
Put Nginx in front of the application.
Enable HTTPS.
Make sure the server is reasonably secure.
Let’s get started.
Prerequisites
Before we start, make sure you have:
An application ready to deploy.
A
docker-compose.prod.ymlfile.A Contabo VPS. For this guide, I’m using the lowest instance.
Your application available on GitHub.
A domain name.
And, of course, you’re ready to learn. 😄
The Steps We’ll Follow
Here’s what we’re going to do:
Access the VPS through SSH as root.
Create a new user.
Set up SSH access for the new user.
Create a server setup script.
Run the script.
Configure the firewall.
Pull the application from GitHub.
Configure the environment variables.
Deploy Docker.
Configure Nginx.
Connect the domain.
Generate the SSL certificate.
Disable root SSH access.
Test everything.
Nothing too complicated. Let’s take it one step at a time.
1. Access the VPS Through SSH
First, log in to yourContabodashboard.
On the left sidebar, click:
Servers&Hosting → VPS
Servers&Hosting → VPS

IP address
Select your instance and get theIP address.
Press enter or click to view image in full size

You’ll also need the root password.
If you don’t have the password, select your instance and click the three dots at the end.
Press enter or click to view image in full size

Reset Credentials
ChooseReset Credentialsand set a new password.

Once you’ve done that, open your terminal.
Run:
ssh root@YOUR_SERVER_IPFor example:
ssh root@123.45.678.912Enter the root password when prompted.
And there we go — we’re inside the server.
Press enter or click to view image in full size

2. Create a User Instead of Using Root
Now that we have access to root, there’s something we need to fix.
Using root for everything isn’t a good idea.
We’re going to create another user that we’ll use for our normal deployment work. Whenever we need administrative access, we’ll usesudo.
I’ll call my userdeploy.

Run:
useradd -m -s /bin/bash -G sudo,docker deploySet a password:
passwd deployEnter your desired password twice.
Now let’s create the SSH directory:
mkdir -p /home/deploy/.ssh chmod 700 /home/deploy/.sshNow we need to configure SSH.
Generate an SSH key:
# Don't generate a private key on the VPS and copy it back to your laptop. # Generate it on the laptop and put only the public key on the VPS ssh-keygen -t ed25519 -C "deploy@example.com"Save it under:
/home/deploy/.ssh/id_ed25519Then copy the public key intoauthorized_keys:
cp /home/deploy/.ssh/id_ed25519.pub /home/deploy/.ssh/authorized_keysSet the correct permissions:
chmod 600 /home/deploy/.ssh/authorized_keys chown -R deploy:deploy /home/deploy/.sshNow let’s get the private key:
cat /home/deploy/.ssh/id_ed25519Copy the entire output.
Keep this private key private. Don’t send it to anyone or commit it to GitHub.
Save the Key on Your Mac
On your Mac, create a file for the SSH key:
nano ~/.ssh/contabo-deployPaste the private key.
Save the file and then run:
chmod 600 ~/.ssh/contabo-deployNow let’s make connecting to the server easier.
Open your SSH config:
nano ~/.ssh/configAdd:
Host myserver HostName YOUR_SERVER_IP User deploy IdentityFile ~/.ssh/contabo-deployNow instead of typing the full SSH command, we can simply run:
ssh myserverIf you successfully get into the server asdeploy, we're good to go.
3. Create the Server Setup Script
Now let’s prepare the server.
We could install everything manually, but I prefer using a small script.
That way, if I need to set up another server later, I don’t have to remember every single command.
Create the script:
nano setup-server.shPaste the following:
#!/bin/bash set -eecho "[1/5] Updating system packages..."apt-get update apt-get upgrade -y echo "[2/5] Installing essentials..."apt-get install -y \ curl \ git \ ufw \ nginx \ certbot \ python3-certbot-nginx \ fail2ban echo "[3/5] Installing Docker..."if ! command -v docker>/dev/null 2>&1;
then curl -fsSL https://get.docker.com | sh systemctl enable docker systemctl start docker else echo "Docker already installed." fiecho "Docker version:" docker --versionecho "Docker Compose version:" docker compose version echo "[4/5] Configuring firewall..."ufw default deny incoming ufw default allow outgoingufw allow 22/tcp ufw allow 80/tcp ufw allow 443/tcpufw --force enableecho "Firewall status:" ufw status echo "[5/5] Creating application directory..."mkdir -p /home/deploy/appschown -R deploy:deploy /home/deploy/apps echo "" echo "==============================" echo " Server setup complete!" echo "=============================="Save the file.
Then make it executable:
chmod 700 setup-server.shRun the script:
sudo bash setup-server.shNow we wait.
The script is going to update the server, install the tools we need, install Docker, configure UFW, install Fail2Ban, and create our application directory.
This saves us from manually running all those commands every time.
4. A Quick Note About the Firewall
UFW
The script also sets upUFW, which is our firewall.
For now, we’re allowing three ports:
22 → SSH 80 → HTTP 443 → HTTPSEverything else coming into the server is denied by default.
Fail2Ban
We also installedFail2Ban, which helps protect services such as SSH from repeated brute-force login attempts.
You can check the firewall with:
sudo ufw statusYou should see something similar to:
22/tcp ALLOW 80/tcp ALLOW 443/tcp ALLOW5. Pull the Application From GitHub
Now that the server is ready, let’s bring our application in.
For me, I normally pull the application from GitHub.
First, switch to our deployment user:
su - deployGo to the application directory:
cd ~/appsNow generate an SSH key for GitHub:
ssh-keygen -t ed25519 -C "your-email@example.com"Once it’s created, display the public key:
cat ~/.ssh/id_ed25519.pubCopy the output.
Go to GitHub and add the key under:
Settings → SSH and GPG keys
Settings → SSH and GPG keys
Make sure you’re adding it to your GitHub account, not the repository settings.
You can test the connection:
ssh -T git@github.comIf GitHub recognizes you, we’re ready.
Now clone the application:
cd ~/appsgit clone git@github.com:USERNAME/REPOSITORY.gitThen move into the project:
cd REPOSITORYAnd there we go.
Our application is now on the server.
6. Set Up the Environment
Before starting Docker, we need to configure our production environment.
For example:
nano .envAdd the environment variables your application needs.
This could include things like:
DATABASE_URL=... SECRET_KEY=... API_KEY=... #then after chmod 600 .envMake sure you don’t accidentally commit your production.envfile to GitHub.
This is where you’ll also configure your database, storage, email service, API keys, and any other production services your application depends on.
7. Deploy Docker
Now we’re finally ready to deploy.
From inside your application directory, run:
docker compose -f docker-compose.prod.yml up -d --buildDocker will build the images and start the containers.
Once that’s finished, let’s check what’s running:
docker compose -f docker-compose.prod.yml psYou can also check your images:
docker imagesIf something isn’t working, don’t panic.
The first thing I normally check is the logs:
docker logs CONTAINER_NAMEOr, since we’re using Docker Compose:
docker compose -f docker-compose.prod.yml logsFor a specific service:
docker compose -f docker-compose.prod.yml logs SERVICE_NAMETake a moment here and make sure the containers are actually running before moving on.
8. Configure Nginx
Now our application is running inside Docker.
But we don’t want users accessing the application directly through something like:
http://YOUR_SERVER_IP:8000Instead, we’ll put Nginx in front of it.
Create an Nginx configuration file:
sudo nano /etc/nginx/sites-available/myappAdd:
server { listen 80;server_name yourdomain.com;
location /.well-known/acme-challenge/ { root /var/www/certbot; } location / {
proxy_pass http://127.0.0.1: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;
proxy_read_timeout 120s;
proxy_connect_timeout 10s;
}
}Replace:
yourdomain.comwith your actual domain.
Also, make sure8000matches the port your Docker application is exposing.
Now enable the configuration:
sudo ln -s /etc/nginx/sites-available/myapp \ /etc/nginx/sites-enabled/myappCreate the Certbot directory:
sudo mkdir -p /var/www/certbotBefore reloading Nginx, let’s test the configuration:
sudo nginx -tIf you get:
syntax is ok test is successfulthen we’re good.
Reload Nginx:
sudo systemctl reload nginx9. Point Your Domain to the Server
Now let’s connect our domain.
For this example, I’m using Cloudflare.
A record
Create anA recordthat points your domain to the Contabo VPS IP.
For example:
Type: A Name: api IPv4 address: YOUR_SERVER_IPSo:
api.myapp.com → YOUR_SERVER_IPOnce DNS has propagated, test the domain:
curl -I http://api.myapp.comYou can also test the server directly:
curl -s -o /dev/null -w "%{http_code}" http://YOUR_SERVER_IP/Depending on your application, you might get200,301,404, or another expected response.
The important thing is that we’re actually reaching the server.
10. Generate the SSL Certificate
Now let’s get HTTPS working.
We’ll use Certbot for this.
Run:
sudo certbot --nginx -d api.myapp.comReplaceapi.myapp.comwith your actual domain.
If everything is configured correctly, Certbot should generate the certificate and update Nginx for us.
Once it finishes, test HTTPS:
curl -I https://api.myapp.comYou should get a successful response.
For example:
HTTP/2 200You can also open the domain in your browser.
If you see your application with the padlock 🔒, we’re doing well.
11. Disable Root SSH Access
Remember at the beginning when we logged in as root?
Now that our deployment user is working, we don’t need to leave root SSH access enabled.
Before doing this, make absolutely sure you can log in using your deployment user.
Before doing this, make absolutely sure you can log in using your deployment user.
From your local machine, test:
ssh myserverIf that works, we’re safe to continue.
Open the SSH configuration:
sudo nano /etc/ssh/sshd_configMake sure these settings are present:
PermitRootLogin no PasswordAuthentication noThis means root won’t be able to log in through SSH, and password-based SSH authentication will be disabled.
You can also apply the changes using:
sudo nano /etc/ssh/sshd_config.d/99-hardening.conf # then add PermitRootLogin no PasswordAuthentication no PubkeyAuthentication yes # or one line though not advised sudo sed -i 's/^#\?PermitRootLogin.*/PermitRootLogin no/' /etc/ssh/sshd_configsudo sed -i 's/^#\?PasswordAuthentication.*/PasswordAuthentication no/' /etc/ssh/sshd_configNow let’s rate-limit SSH connections using UFW:
sudo ufw delete allow 22/tcp sudo ufw limit 22/tcpRestart SSH:
sudo systemctl restart sshFinally, let’s make sure Fail2Ban is running:
sudo systemctl status fail2banThat’s All
And that’s it.

We started with a fresh Contabo VPS and ended up with:
Docker running our application
A dedicated deployment user
SSH key authentication
Root SSH access disabled
UFW configured
Fail2Ban running
Nginx configured
Our domain pointing to the VPS
HTTPS enabled
Our application running in production
There are still a few things we can improve from here — automated deployments, backups, monitoring, Docker health checks, Cloudflare configuration, database backups, and maybe even zero-downtime deployments.
You can leave a comment on how we can do it better.