First Deployment Tutorial
This step-by-step tutorial will guide you through your first complete deployment with Portoser, from setup to monitoring.
What We'll Build
We'll deploy a simple web application with:
- A web server (nginx)
- A backend API (Python FastAPI)
- A database (PostgreSQL)
- Health monitoring
- Vault secrets
Prerequisites
- Portoser installed (Installation Guide)
- Two machines accessible via SSH (can be VMs or physical)
- Basic familiarity with terminal/command line
Step 1: Prepare Your Machines
Machine 1 (m1): Main Server
- Host: 192.168.0.10
- User: admin
- Will host: Database and API
Machine 2 (mini1): Web Server
- Host: 192.168.0.20
- User: ubuntu
- Will host: Nginx web server
Test SSH Access
# Test Machine 1
ssh admin@192.168.0.10
exit
# Test Machine 2
ssh ubuntu@192.168.0.20
exit
# Set up passwordless SSH (recommended)
ssh-copy-id admin@192.168.0.10
ssh-copy-id ubuntu@192.168.0.20
Step 2: Register Machines
# Register Machine 1
portoser machine add \
--name m1 \
--host 192.168.0.10 \
--user admin \
--platform linux
# Verify it was added
portoser machine list
# Register Machine 2
portoser machine add \
--name mini1 \
--host 192.168.0.20 \
--user ubuntu \
--platform linux
# Verify both machines
portoser machine list
Expected Output:
MACHINE HOST USER PLATFORM STATUS
m1 192.168.0.10 admin linux ✓ Connected
mini1 192.168.0.20 ubuntu linux ✓ Connected
Step 3: Prepare Service Code
Create PostgreSQL Service
On m1, create directory:
ssh admin@192.168.0.10
mkdir -p /opt/postgres
cd /opt/postgres
Create docker-compose.yml:
version: '3.8'
services:
postgres:
image: postgres:15
environment:
POSTGRES_DB: myapp
POSTGRES_USER: ${POSTGRES_USER}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
volumes:
- postgres_data:/var/lib/postgresql/data
ports:
- "5432:5432"
healthcheck:
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER}"]
interval: 10s
timeout: 5s
retries: 5
volumes:
postgres_data:
Create API Service
On m1, create directory:
mkdir -p /opt/api
cd /opt/api
Create main.py:
from fastapi import FastAPI
import os
app = FastAPI()
@app.get("/")
def read_root():
return {"message": "Hello from API"}
@app.get("/health")
def health():
return {"status": "healthy"}
@app.get("/db")
def check_db():
db_url = os.getenv("DATABASE_URL")
return {"database": "connected" if db_url else "not configured"}
Create pyproject.toml:
[project]
name = "myapi"
version = "0.1.0"
dependencies = [
"fastapi",
"uvicorn",
]
Create Web Service
On mini1, create directory:
ssh ubuntu@192.168.0.20
mkdir -p /opt/web
cd /opt/web
Create docker-compose.yml:
version: '3.8'
services:
nginx:
image: nginx:alpine
ports:
- "80:80"
volumes:
- ./nginx.conf:/etc/nginx/nginx.conf:ro
healthcheck:
test: ["CMD", "wget", "-q", "--spider", "http://localhost/health"]
interval: 10s
timeout: 3s
retries: 3
Create nginx.conf:
events {
worker_connections 1024;
}
http {
server {
listen 80;
location /health {
return 200 "healthy\n";
add_header Content-Type text/plain;
}
location / {
proxy_pass http://192.168.0.10:8000;
}
}
}
Exit SSH sessions:
exit
Step 4: Set Up Vault (Optional but Recommended)
Install and Start Vault
# On your local machine or a dedicated server
vault server -dev
# In another terminal
export VAULT_ADDR='http://127.0.0.1:8200'
vault login <dev-root-token>
Store Database Credentials
# Store PostgreSQL credentials
vault kv put secret/services/postgres \
POSTGRES_USER=myapp_user \
POSTGRES_PASSWORD=secure_password_123
Configure Portoser for Vault
export VAULT_ADDR='http://127.0.0.1:8200'
export VAULT_TOKEN='<your-vault-token>'
Step 5: Register Services
Register PostgreSQL
portoser service add \
--name postgres \
--type docker \
--path /opt/postgres \
--machine m1 \
--port 5432 \
--health-type tcp \
--health-endpoint "5432"
If using Vault, add secrets reference to registry:
# Edit registry.yml
nano registry.yml
Add under postgres service:
services:
postgres:
# ... existing config ...
vault_secrets:
- path: "secret/data/services/postgres"
keys:
- POSTGRES_USER
- POSTGRES_PASSWORD
Register API Service
portoser service add \
--name api \
--type local \
--path /opt/api \
--machine m1 \
--port 8000 \
--command "uv run uvicorn main:app --host 0.0.0.0 --port 8000" \
--health-endpoint "/health" \
--health-type http \
--depends-on postgres
Add database URL to API:
nano registry.yml
services:
api:
# ... existing config ...
env:
DATABASE_URL: "postgresql://myapp_user:secure_password_123@localhost:5432/myapp"
depends_on:
- postgres
Register Web Service
portoser service add \
--name web \
--type docker \
--path /opt/web \
--machine mini1 \
--port 80 \
--health-endpoint "/health" \
--health-type http \
--depends-on api
Verify Registration
portoser service list
Expected Output:
SERVICE TYPE MACHINE PORT STATUS HEALTH
postgres docker m1 5432 not started -
api local m1 8000 not started -
web docker mini1 80 not started -
Step 6: Deploy Services
Deploy with Intelligent Mode
# Deploy postgres first (no dependencies)
portoser deploy postgres --intelligent
# Deploy api (depends on postgres)
portoser deploy api --intelligent
# Deploy web (depends on api)
portoser deploy web --intelligent
Watch the Deployment
During deployment, you'll see:
[OBSERVE] Connecting to m1 (192.168.0.10)...
[OBSERVE] SSH connection successful
[OBSERVE] Disk space: 45GB available
[OBSERVE] Port 5432: Available
[OBSERVE] Docker daemon: Running
[DEPLOY] Starting Docker Compose deployment
[DEPLOY] Pulling images...
[DEPLOY] Creating containers...
[DEPLOY] Starting postgres...
[HEALTH] Checking service health...
[HEALTH] Service is healthy ✓
[STANDARDIZE] Recording successful deployment
✓ postgres deployed successfully
Step 7: Verify Deployments
Check Service Status
# Check all services
portoser status --all
Expected Output:
SERVICE TYPE MACHINE PORT STATUS HEALTH
postgres docker m1 5432 running ✓ healthy
api local m1 8000 running ✓ healthy
web docker mini1 80 running ✓ healthy
Test Health Checks
# Check individual service health
portoser health check postgres
portoser health check api
portoser health check web
# Or check all at once
portoser health check --all
Test the Application
# Test API directly
curl http://192.168.0.10:8000/health
# Test through web server
curl http://192.168.0.20/health
# Test API through web server
curl http://192.168.0.20/
Step 8: Monitor Services
View Logs
# View API logs
portoser logs api --tail 50
# Follow logs in real-time
portoser logs api --follow
# View postgres logs
portoser logs postgres --tail 20
Monitor Health
# Continuously monitor API health
portoser health monitor api
# In another terminal, monitor web
portoser health monitor web
View Deployment History
# View all deployments
portoser history list
# View specific service history
portoser history list api
Step 9: Make a Change
Let's update the API and redeploy.
Update API Code
SSH to m1:
ssh admin@192.168.0.10
cd /opt/api
nano main.py
Add a new endpoint:
@app.get("/version")
def version():
return {"version": "1.1.0"}
Save and exit:
exit
Redeploy
# Restart the API service to pick up changes
portoser restart api --intelligent
# Test the new endpoint
curl http://192.168.0.10:8000/version
Step 10: Use the Web UI (Optional)
If you've installed the web UI:
cd web
docker-compose up -d
Open http://localhost:8080 in your browser.
You'll see:
- All three services in a visual dashboard
- Machine cards showing m1 and mini1
- Health indicators
- Ability to drag services between machines
- Real-time logs and metrics
Try:
- Click on a service to view details
- Right-click for quick actions menu
- Drag API service to mini1 (Portoser will redeploy it)
- View deployment history
- Check dependency graph
Troubleshooting
Service Won't Start
# Run diagnostics
portoser diagnose postgres
# Check logs
portoser logs postgres --tail 100
# Try intelligent deployment
portoser deploy postgres --intelligent --verbose
Port Already in Use
Intelligent deployment will detect and fix this:
portoser deploy api --intelligent
# Output will show:
# [DIAGNOSE] Port 8000 conflict detected
# [SOLVE] Stopping process on port 8000
# [DEPLOY] Proceeding with deployment
Dependency Health Check Failed
# Check dependency first
portoser health check postgres
# If unhealthy, check logs
portoser logs postgres
# Restart dependency
portoser restart postgres
# Then deploy dependent service
portoser deploy api
SSH Connection Failed
# Test SSH manually
ssh admin@192.168.0.10
# Check machine configuration
portoser machine show m1
# Update if IP changed
portoser machine update m1 --host new-ip-address
Next Steps
Congratulations! You've successfully:
- ✓ Registered machines
- ✓ Registered services
- ✓ Deployed services with dependencies
- ✓ Verified health
- ✓ Monitored logs
- ✓ Made changes and redeployed
Now you can:
- Set up Caddy for automatic HTTPS
- Configure Vault for better secret management
- Set up monitoring with alerts
- Explore intelligent deployment capabilities
- Use the Web UI for visual management
Common Patterns
Blue-Green Deployment
# Deploy new version to different machine
portoser service update api --machine mini1
portoser deploy api
# Test new version
curl http://192.168.0.20:8000/health
# If good, update web to point to new API
# If bad, rollback
portoser history rollback api
Rolling Restart
# For services with multiple instances
portoser restart api --intelligent --rolling
Scheduled Deployments
# Add to crontab
0 2 * * * /usr/local/bin/portoser deploy api --intelligent
Backup Before Deploy
# Create backup
portoser backup api
# Deploy
portoser deploy api
# If needed, restore
portoser restore api