> ## Documentation Index
> Fetch the complete documentation index at: https://docs.aspfox.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Deploy to VPS with Docker

> Ubuntu 22.04 VPS deployment with Docker Compose, Nginx reverse proxy, and Let's Encrypt SSL.

This guide uses Ubuntu 22.04. Every command is exact and copy-paste ready.

<Steps>
  <Step title="Provision a VPS">
    Any provider works: DigitalOcean, Hetzner, Linode, Vultr. Minimum recommended: 2 vCPU, 2 GB RAM, 20 GB SSD. Ubuntu 22.04 LTS.

    After provisioning, SSH in as root:

    ```bash theme={null}
    ssh root@your-server-ip
    ```

    Create a non-root user:

    ```bash theme={null}
    adduser deploy
    usermod -aG sudo deploy
    rsync --archive --chown=deploy:deploy ~/.ssh /home/deploy
    ```

    Switch to the deploy user for the rest of this guide:

    ```bash theme={null}
    su - deploy
    ```
  </Step>

  <Step title="Install Docker">
    ```bash theme={null}
    # Install prerequisites
    sudo apt-get update
    sudo apt-get install -y ca-certificates curl gnupg

    # Add Docker's GPG key
    sudo install -m 0755 -d /etc/apt/keyrings
    curl -fsSL https://download.docker.com/linux/ubuntu/gpg \
      | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg
    sudo chmod a+r /etc/apt/keyrings/docker.gpg

    # Add the Docker repository
    echo \
      "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] \
      https://download.docker.com/linux/ubuntu \
      $(. /etc/os-release && echo "$VERSION_CODENAME") stable" | \
      sudo tee /etc/apt/sources.list.d/docker.list > /dev/null

    # Install Docker Engine and Compose plugin
    sudo apt-get update
    sudo apt-get install -y docker-ce docker-ce-cli containerd.io \
      docker-buildx-plugin docker-compose-plugin

    # Allow deploy user to run Docker without sudo
    sudo usermod -aG docker deploy
    newgrp docker

    # Verify
    docker --version
    docker compose version
    ```
  </Step>

  <Step title="Clone your repository">
    ```bash theme={null}
    cd /opt
    sudo mkdir acme
    sudo chown deploy:deploy acme
    cd acme
    git clone https://github.com/yourorg/youracme-repo.git .
    ```
  </Step>

  <Step title="Configure environment variables">
    ```bash theme={null}
    cp .env.example .env
    nano .env
    ```

    Fill in all production values. Key differences from local development:

    ```bash theme={null}
    ASPNETCORE_ENVIRONMENT=Production
    DATABASE_URL=Host=postgres;Port=5432;Database=acme;Username=acme;Password=<strong-password>
    REDIS_URL=redis:6379
    APP_URL=https://yourdomain.com
    API_URL=https://api.yourdomain.com
    VITE_API_URL=https://api.yourdomain.com
    ```
  </Step>

  <Step title="Generate production JWT keys">
    ```bash theme={null}
    openssl genrsa -out private.pem 4096
    openssl rsa -in private.pem -pubout -out public.pem
    base64 -w 0 private.pem
    base64 -w 0 public.pem
    ```

    Copy the base64 output into `.env` as `JWT_PRIVATE_KEY` and `JWT_PUBLIC_KEY`, then delete the key files:

    ```bash theme={null}
    rm private.pem public.pem
    ```
  </Step>

  <Step title="Start services with production Docker Compose">
    ```bash theme={null}
    docker compose -f docker-compose.prod.yml up -d
    ```

    Check that all containers are running:

    ```bash theme={null}
    docker compose -f docker-compose.prod.yml ps
    ```

    You should see `api`, `frontend`, `postgres`, and `redis` all in `running` state.
  </Step>

  <Step title="Run migrations and seed">
    ```bash theme={null}
    docker compose -f docker-compose.prod.yml exec api \
      dotnet ef database update \
      --project src/Acme.Infrastructure \
      --startup-project src/Acme.Api

    docker compose -f docker-compose.prod.yml exec api \
      dotnet run --project src/Acme.Api -- --seed-only
    ```
  </Step>

  <Step title="Install and configure Nginx">
    ```bash theme={null}
    sudo apt-get install -y nginx
    ```

    Create the Nginx configuration:

    ```bash theme={null}
    sudo nano /etc/nginx/sites-available/acme
    ```

    Paste this configuration (replace `yourdomain.com` and `api.yourdomain.com` with your actual domains):

    ```nginx theme={null}
    # API
    server {
        listen 80;
        server_name api.yourdomain.com;

        location / {
            proxy_pass http://localhost:5000;
            proxy_http_version 1.1;
            proxy_set_header Upgrade $http_upgrade;
            proxy_set_header Connection keep-alive;
            proxy_set_header Host $host;
            proxy_set_header X-Real-IP $remote_addr;
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
            proxy_set_header X-Forwarded-Proto $scheme;
            proxy_cache_bypass $http_upgrade;
            proxy_read_timeout 300s;
            proxy_connect_timeout 75s;
        }
    }

    # Frontend
    server {
        listen 80;
        server_name yourdomain.com;

        location / {
            proxy_pass http://localhost:5173;
            proxy_http_version 1.1;
            proxy_set_header Host $host;
            proxy_set_header X-Real-IP $remote_addr;
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
            proxy_set_header X-Forwarded-Proto $scheme;
        }
    }
    ```

    Enable the configuration:

    ```bash theme={null}
    sudo ln -s /etc/nginx/sites-available/acme /etc/nginx/sites-enabled/
    sudo nginx -t    # Test the configuration — must print "syntax is ok"
    sudo systemctl reload nginx
    ```
  </Step>

  <Step title="Set up SSL with Let's Encrypt">
    ```bash theme={null}
    sudo apt-get install -y certbot python3-certbot-nginx

    sudo certbot --nginx \
      -d yourdomain.com \
      -d api.yourdomain.com \
      --non-interactive \
      --agree-tos \
      --email your@email.com
    ```

    Certbot modifies the Nginx configuration to add HTTPS and sets up automatic renewal. Verify:

    ```bash theme={null}
    sudo certbot renew --dry-run
    ```
  </Step>

  <Step title="Configure Stripe production webhook">
    Now that your API has a public HTTPS URL:

    1. Stripe Dashboard → **Developers → Webhooks** → **Add endpoint**
    2. URL: `https://api.yourdomain.com/api/v1/webhooks/stripe`
    3. Events: checkout.session.completed, customer.subscription.updated, customer.subscription.deleted, invoice.payment\_failed, invoice.payment\_succeeded
    4. Copy signing secret → update `STRIPE_WEBHOOK_SECRET` in `.env`
    5. Restart the API: `docker compose -f docker-compose.prod.yml restart api`
  </Step>

  <Step title="Verify">
    ```bash theme={null}
    curl https://api.yourdomain.com/health
    # Should return: {"status":"healthy"}
    ```

    Open `https://yourdomain.com` in a browser and log in with admin credentials.
  </Step>
</Steps>

## Keeping the application updated

To deploy a new version:

```bash theme={null}
cd /opt/acme
git pull origin main
docker compose -f docker-compose.prod.yml build api frontend
docker compose -f docker-compose.prod.yml up -d
# Run migrations if schema changed:
docker compose -f docker-compose.prod.yml exec api \
  dotnet ef database update \
  --project src/Acme.Infrastructure \
  --startup-project src/Acme.Api
```

## Viewing production logs

```bash theme={null}
# All services
docker compose -f docker-compose.prod.yml logs -f

# API only
docker compose -f docker-compose.prod.yml logs -f api
```
