Contents
What you need to start
- A VPS with Ubuntu 22.04 (any provider: Hetzner, DigitalOcean, Timeweb)
- A domain or just an IP address
- Locally: an SSH client and a repo with a
Dockerfile+docker-compose.yml
Connecting to the server
ssh root@YOUR_SERVER_IP
First thing — update the system and create a user instead of root:
apt update && apt upgrade -y
# Create a user
adduser deploy
usermod -aG sudo deploy
# Copy the SSH key for the new user
rsync --archive --chown=deploy:deploy ~/.ssh /home/deploy
Installing Docker
# Official install script
curl -fsSL https://get.docker.com | sh
# Add the user to the docker group (no sudo needed)
usermod -aG docker deploy
# Verify
docker --version
docker compose version
💡 After
usermodyou need to log out and back in:exitand thenssh deploy@IP.
Cloning the project to the server
# On the server
cd /home/deploy
git clone https://github.com/your-user/your-repo.git app
cd app
If the repo is private — add the server's SSH key to GitHub:
# Generate the key on the server
ssh-keygen -t ed25519 -C "deploy@server"
# Copy the public key
cat ~/.ssh/id_ed25519.pub
# Add it to GitHub → Settings → Deploy keys
Configuring environment variables
# On the server create .env (don't commit it to git!)
cp .env.example .env
nano .env
# .env on the server
POSTGRES_USER=myuser
POSTGRES_PASSWORD=strong_password_here
POSTGRES_DB=myapp
APP_PORT=3000
NODE_ENV=production
Starting the app
# Build images and start in the background
docker compose up -d --build
# Check status
docker compose ps
# View logs
docker compose logs -f app
If everything's up — the app is reachable at http://IP:3000.
Nginx as a reverse proxy
Nginx accepts requests on 80/443 and proxies them into the container. Without it you don't get HTTPS or a clean domain.
Add Nginx to docker-compose.yml:
services:
app:
build: .
expose:
- "3000" # Port exposed only inside the Docker network
networks:
- web
nginx:
image: nginx:alpine
ports:
- "80:80"
- "443:443"
volumes:
- ./nginx/default.conf:/etc/nginx/conf.d/default.conf:ro
- ./nginx/certs:/etc/nginx/certs:ro
depends_on:
- app
networks:
- web
networks:
web:
driver: bridge
The nginx/default.conf config:
server {
listen 80;
server_name yourdomain.com;
location / {
proxy_pass http://app:3000;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade';
proxy_set_header Host $host;
proxy_cache_bypass $http_upgrade;
}
}
💡 For HTTPS — use Certbot or swap Nginx for Caddy: it grabs an SSL certificate automatically with no config.
Auto-deploy via GitHub Actions
Every push to main triggers an automatic deploy to the server (Actions basics are in a separate article).
Create .github/workflows/deploy.yml:
name: Deploy
on:
push:
branches: [main]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- name: Deploy via SSH
uses: appleboy/ssh-action@v1
with:
host: ${{ secrets.SERVER_HOST }}
username: ${{ secrets.SERVER_USER }}
key: ${{ secrets.SERVER_SSH_KEY }}
script: |
cd /home/deploy/app
git pull origin main
docker compose up -d --build
docker image prune -f
Add the secrets in GitHub → Settings → Secrets and variables → Actions:
SERVER_HOST— server IPSERVER_USER—deploySERVER_SSH_KEY— private key (cat ~/.ssh/id_ed25519)
Useful server-side commands
# See what's taking up space
docker system df
# Remove unused images and layers
docker system prune -f
# Restart one service without downtime
docker compose up -d --no-deps --build app
# Shell into a container
docker compose exec app sh
# Database backup
docker compose exec db pg_dump -U myuser myapp > backup_$(date +%Y%m%d).sql
⚠️ Always check
docker compose logsafter deploy — the container can start but crash inside.
Discussion
No comments yet. Be the first.