Skip to main content

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.

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

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:
ssh root@your-server-ip
Create a non-root user:
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:
su - deploy
2

Install Docker

# 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
3

Clone your repository

cd /opt
sudo mkdir acme
sudo chown deploy:deploy acme
cd acme
git clone https://github.com/yourorg/youracme-repo.git .
4

Configure environment variables

cp .env.example .env
nano .env
Fill in all production values. Key differences from local development:
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
5

Generate production JWT keys

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:
rm private.pem public.pem
6

Start services with production Docker Compose

docker compose -f docker-compose.prod.yml up -d
Check that all containers are running:
docker compose -f docker-compose.prod.yml ps
You should see api, frontend, postgres, and redis all in running state.
7

Run migrations and seed

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
8

Install and configure Nginx

sudo apt-get install -y nginx
Create the Nginx configuration:
sudo nano /etc/nginx/sites-available/acme
Paste this configuration (replace yourdomain.com and api.yourdomain.com with your actual domains):
# 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:
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
9

Set up SSL with Let's Encrypt

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:
sudo certbot renew --dry-run
10

Configure Stripe production webhook

Now that your API has a public HTTPS URL:
  1. Stripe Dashboard → Developers → WebhooksAdd 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
11

Verify

curl https://api.yourdomain.com/health
# Should return: {"status":"healthy"}
Open https://yourdomain.com in a browser and log in with admin credentials.

Keeping the application updated

To deploy a new version:
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

# All services
docker compose -f docker-compose.prod.yml logs -f

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