I recently decided to host one of my Dockerized Node.js apps on a VPS using Oracle Cloud Free Tier - not just to deploy it, but to understand what’s actually happening under the hood.
I wanted to learn:
- How to create and configure a compute instance
- How Docker behaves on a VPS
- Why backend ports shouldn't be exposed directly
- How NGINX fits into the picture
- What restart policies actually do
Here's exactly how I set it up - and what I learned along the way.
Step 1 - Create a Compute Instance on Oracle Cloud
Inside Oracle Cloud:
- Go to Compute -> Instances
- Click Create Instance
- Choose Ubuntu (I used 22.04)
- Select an Always Free eligible shape (1 vCPU, ~1GB RAM)
When creating the instance, upload your SSH public key or download the key Oracle provides.
Once the instance is running:
ssh -i your-key.pem ubuntu@YOUR_SERVER_IP
Now you're inside a real Linux server - not a managed environment.
Step 2 - Basic Setup & Firewall
First, update the system:
sudo apt update
sudo apt upgrade -y
Then enable UFW:
sudo apt install ufw -y
sudo ufw allow OpenSSH
sudo ufw enable
At this point:
- Only SSH is allowed
- Everything else is blocked
Important lesson:
Oracle Cloud has its own Security List (cloud firewall).
Ubuntu has UFW (OS firewall).
If something doesn't work later, check both.
Step 3 - Install Docker
Install Docker
curl -fsSL https://get.docker.com -o get-docker.sh
sudo sh get-docker.sh
After installing Docker, I added my user to the docker group:
sudo usermod -aG docker $USER
exit
Reconnect and test:
docker run hello-world
Docker is now ready to run containers.
Step 4 - Create a Private Docker Network
Before running anything, I created a custom Docker network:
docker network create web
This was important.
Instead of exposing my Node.js app like this:
docker run -p 3000:3000 my-app
I wanted the app to stay internal and let NGINX handle public traffic.
Containers attached to the same custom network:
- Can talk to each other using their names
- Are not exposed publicly unless you publish ports
Think of it as a private network inside your VPS.
Step 5 - Install NGINX Reverse Proxy
At this point, the Node.js app is internal. But something still needs to handle incoming HTTP requests from the outside world. That’s where NGINX comes in.
I used NGINX Proxy Manager for simplicity.
Create a directory for it in VPS:
mkdir nginx-proxy
cd nginx-proxy
Then create docker-compose.yml
Here's the simplified docker-compose.yml:
version: "3.8"
services:
npm:
image: jc21/nginx-proxy-manager:latest
container_name: nginx-proxy
restart: unless-stopped
ports:
- "80:80"
- "443:443"
- "81:81"
volumes:
- ./data:/data
- ./letsencrypt:/etc/letsencrypt
networks:
- web
networks:
web:
external: true
networks: web
This attaches NGINX to the same private network as the app.
So it can forward traffic internally.
volumes
This ensures configuration and SSL certificates survive container restarts.
restart: unless-stopped
It means:
- If the VPS reboots -> the container starts automatically
- If Docker restarts -> container comes back
- If it crashes -> it restarts
- If you manually stop it -> it stays stopped
Then start it:
docker compose up -d
Now NGINX is running.
Step 6 - Open Ports in Oracle Cloud
Open ports in OS level
sudo ufw allow 80
sudo ufw allow 443
Even after configuring UFW, I couldn't access the server from the browser.
That's because Oracle Cloud blocks traffic by default.
I had to allow:
- Port 80
- Port 443
Inside:
Networking - VCN -> Security List
This reinforced something important:
Cloud firewall and OS firewall are separate layers.
Step 7 - Deploy the Dockerized Node.js App
I pushed my Node.js image to a container registry (I used Docker Hub).
On the VPS:
docker login
docker pull myusername/my-node-app:latest
Then created a folder:
mkdir app
cd app
Created .env:
NODE_ENV=production
PORT=3000
Then docker-compose.yml:
version: "3.8"
services:
node-app:
image: myusername/my-node-app:latest
container_name: node-app
restart: unless-stopped
env_file:
- .env
networks:
- web
networks:
web:
external: true
Notice something important:
There is no -p 3000:3000.
The Node.js app is running - but only inside the Docker network.
It is not publicly accessible.
Start it:
docker compose up -d
Step 8 - Connect NGINX to the Node.js App
Inside NGINX Proxy Manager:
- Add Proxy Host
-
Forward Hostname:
node-app -
Forward Port:
3000 - Enable SSL (if using a domain)
Now traffic flows like this:
Browser -> NGINX -> Node.js container
Port 3000 remains internal.
This separation reduces your public attack surface. Only NGINX is exposed. Your application server stays internal.
Free Tier Reality
Oracle Free Tier gives ~1GB RAM.
You can monitor usage:
htop
docker stats
This comfortably runs:
- NGINX
- One or two small Node.js apps
What I Learned From This
Setting this up myself helped me understand:
- What a compute instance actually is
- Why reverse proxies are important
- How Docker networking isolates services
- Why backend ports shouldn't be exposed
- How restart policies affect uptime
- How cloud and OS firewalls interact
It's very different from just deploying code.
You start understanding the system your app runs on.
And that's what made this exercise worth it.
Top comments (0)