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:

  1. Click on a service to view details
  2. Right-click for quick actions menu
  3. Drag API service to mini1 (Portoser will redeploy it)
  4. View deployment history
  5. 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:

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