Installing Docker on a VPS: a step-by-step guide for web applications

Docker eliminates the classic problem of it worked on my machine: an application along with all its dependencies is packaged into a container and runs identically anywhere. For web projects, it also means simple deployment — an update comes down to rebuilding the image and restarting the container.
Docker can only run where there is access to the system kernel, meaning on a VPS or dedicated server: on shared web hosting it is simply not possible. Below is the full path from renting a server to a working application over HTTPS.
Step 1. Get a server
In the NodexGo control panel, click Create server and go through four steps: location (your real ping to each one is shown next to it), plan, system image, and server name. Then choose a billing period — 1, 3, 6, or 12 months — and confirm the order.
For Docker, choose Ubuntu 24.04: all the commands below are written for it. As for resources, here is a rough guide: a couple of small containers run fine on 2 GB of RAM, while a combination of application plus database plus proxy is comfortable on 4 GB. Building images is disk-intensive, so NVMe noticeably saves time here.
In about a minute the server is ready: the IP address and root password will appear in the panel and be sent to your email.
Ubuntu 24.04, NVMe, and root access — everything Docker needs.
Create a serverStep 2. First connection
Connect via SSH, substituting the provided IP address:
ssh root@server-IP-addressIf SSH is not responding or you have locked yourself out with a firewall rule, the server card in the panel has a VNC web console — it works independently of the network and always lets you into the system.
Update the packages:
apt update && apt upgrade -yStep 3. Install Docker Engine
Installing Docker from the standard Ubuntu repository is not recommended — it almost always contains an outdated version. Connect the official Docker repository. First, install the dependencies and the key:
apt install -y ca-certificates curl gnupg
install -m 0755 -d /etc/apt/keyrings
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | gpg --dearmor -o /etc/apt/keyrings/docker.gpg
chmod a+r /etc/apt/keyrings/docker.gpgAdd the repository itself:
echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu $(. /etc/os-release && echo $VERSION_CODENAME) stable" > /etc/apt/sources.list.d/docker.listAnd install Docker along with the Compose plugin:
apt update
apt install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-pluginVerify that everything is up and running:
docker run --rm hello-world
docker compose versionIf the Docker welcome message and the Compose version are displayed, the engine is working. Enable autostart after a server reboot like this:
systemctl enable --now dockerStep 4. Work without root
There is no need to stay logged in as root on the server all the time. Create a regular user and give them the right to run Docker:
adduser deploy
usermod -aG sudo,docker deployAfter that, reconnect via SSH as that user. Keep in mind: membership in the docker group is effectively equivalent to root privileges — only add users you trust.
Step 5. First application with Compose
Compose describes the entire stack in a single file, so the application is brought up with one command. Create the project directory:
mkdir -p /opt/app && cd /opt/app
nano docker-compose.ymlA minimal example: a Node.js application and a PostgreSQL database. The database password is placed in an environment variable, and the data is stored in a named volume so it survives a container restart.
services:
app:
image: node:22-alpine
working_dir: /srv
volumes:
- ./src:/srv
command: node index.js
environment:
DATABASE_URL: postgres://app:${DB_PASSWORD}@db:5432/app
depends_on:
- db
restart: always
db:
image: postgres:16-alpine
environment:
POSTGRES_USER: app
POSTGRES_PASSWORD: ${DB_PASSWORD}
POSTGRES_DB: app
volumes:
- dbdata:/var/lib/postgresql/data
restart: always
volumes:
dbdata:Place the password in a .env file next to docker-compose.yml — it will not end up in the repository:
echo "DB_PASSWORD=choose-a-long-password" > .env
chmod 600 .envStart the stack in the background and check the result:
docker compose up -d
docker compose ps
docker compose logs -f appStep 6. HTTPS and reverse proxy
There is no need to expose containers directly to the internet. A reverse proxy is placed in front of them, which accepts requests on ports 80 and 443, obtains a Let's Encrypt certificate on its own, and forwards traffic inside. Caddy does this most easily — it issues and renews the certificate automatically.
Add one more service to docker-compose.yml:
proxy:
image: caddy:2-alpine
ports:
- "80:80"
- "443:443"
volumes:
- ./Caddyfile:/etc/caddy/Caddyfile
- caddydata:/data
restart: alwaysAnd create a Caddyfile next to it — two lines, where you replace example.com with your own domain already pointed via an A record to the server IP:
example.com {
reverse_proxy app:3000
}After restarting the stack, the site will open over HTTPS without any manual certificate configuration:
docker compose up -dStep 7. Firewall
Leave only what is truly necessary exposed to the outside: SSH and web. Do not publish the container ports externally — they communicate with each other over the internal Compose network.
ufw allow OpenSSH
ufw allow 80/tcp
ufw allow 443/tcp
ufw enableThere is an important caveat: Docker works with iptables directly and can bypass ufw rules if you publish a port using a string like 5432:5432. Therefore, never publish databases and internal services externally — access them by service name within Compose.
Updating the application and maintenance
Update images and restart the stack:
docker compose pull
docker compose up -dOver time, Docker takes up a lot of space with old images and build layers. It is useful to clean up once a month:
docker system df
docker system prune -aDo not forget about backups: the database volume must be exported separately — the container itself is not a backup.
docker compose exec -T db pg_dump -U app app > /root/backup-$(date +%F).sqlIf resources are insufficient
A sign that the server is too small: containers start crashing with exit code 137 — this means the process was killed by the out-of-memory mechanism. You can check the current load like this:
docker stats --no-stream
free -hIn this situation, the server card in the panel has an Upgrade resources button: the plan is switched to a higher tier, the disk and IP address are preserved, and the containers keep running — no reinstallation is needed.
In brief
Rent a VPS with Ubuntu 24.04, install Docker from the official repository, describe the application in docker-compose.yml, put a reverse proxy with automatic HTTPS in front of it, and leave only ports 22, 80, and 443 open. From that point on, any update is two commands, and migrating to another server comes down to copying the project directory.
Plans from 2 to 32 GB of RAM — start small, upgrade to a higher tier in one click.
View plans