Deploying to a VPS
The simplest production deployment
A VPS (Virtual Private Server) is a Linux machine in the cloud. You SSH into it, install Docker, pull your image, and run it. No Kubernetes. No AWS services. Just Docker on a server.
This is how most small-to-medium apps are deployed. It is simple, cheap ($5-20/month), and sufficient until you need horizontal scaling.
Get a VPS
Providers: DigitalOcean, Hetzner, Linode, Vultr. Create an Ubuntu 22.04+ server with at least 1 GB RAM.
You will receive an IP address and SSH credentials.
Install Docker on the server
# SSH into the server
ssh root@your-server-ip
# Install Docker (official method)
curl -fsSL https://get.docker.com | sh
# Verify
docker --version
docker compose version
# (Optional) Add your user to the docker group
# so you do not need sudo for every docker command
usermod -aG docker your-username Push your image to a registry
You need the image in a registry so the server can pull it. Docker Hub is the simplest:
# On your local machine
# Log in to Docker Hub
docker login
# Tag your image for the registry
docker tag myapp yourusername/myapp:latest
# Push
docker push yourusername/myapp:latest For private images, use GitHub Container Registry (ghcr.io) or a private Docker Hub repository.
Deploy
# On the server
# Pull the image
docker pull yourusername/myapp:latest
# Create the env file
nano .env.production
# Add your production environment variables
# Run
docker run -d \
--name myapp \
-p 80:3000 \
--restart unless-stopped \
--env-file .env.production \
-v app-data:/data \
yourusername/myapp:latest Your app is live at http://your-server-ip.
Using Docker Compose on the server
For multi-container apps, copy the compose file to the server:
# On your local machine
scp docker-compose.yml root@your-server-ip:/app/
scp nginx.conf root@your-server-ip:/app/
scp .env.production root@your-server-ip:/app/
# On the server
cd /app
docker compose up -d Updating the app
# On your local machine
docker build -t yourusername/myapp:latest .
docker push yourusername/myapp:latest
# On the server
docker pull yourusername/myapp:latest
docker compose up -d # Recreates containers with the new image Firewall
Configure the server’s firewall to only allow necessary ports. UFW (Uncomplicated Firewall) is Ubuntu’s built-in firewall tool. By default, a fresh server has all ports open — anyone can probe every port. UFW lets you whitelist specific ports and block everything else:
# UFW — Ubuntu's firewall
ufw allow 22/tcp # SSH — so you can still connect to the server
ufw allow 80/tcp # HTTP — for web traffic and Let's Encrypt verification
ufw allow 443/tcp # HTTPS — for encrypted web traffic
ufw enable # Activate the firewall (blocks all other ports) After ufw enable, only ports 22, 80, and 443 are accessible from the internet. Your app container’s internal port (3000) is blocked — traffic must go through Nginx on port 80/443. This prevents anyone from bypassing the reverse proxy.
Exercises
Exercise 1: Set up a VPS (most providers offer $5/month plans or free trials). Install Docker.
Exercise 2: Push your image to Docker Hub. Pull and run it on the server. Access your app at the server’s IP.
Exercise 3: Set up the firewall. Verify that only ports 22, 80, and 443 are open.
Why deploy with docker pull instead of copying files with scp?