Skip to main content

VPS Deployment with Let's Encrypt

Deploy InstaCRUD to a Virtual Private Server (VPS) with nginx reverse proxy and automatic SSL certificates from Let's Encrypt.


Overview

This setup uses:

  • Docker Compose — Container orchestration
  • nginx — Reverse proxy with SSL termination
  • Certbot — Automatic Let's Encrypt certificate management
  • MongoDB — Database (containerized)
note

The VPS configuration uses .vps suffixed files to keep local development configuration intact.

Easiest Deployment Path

The most convenient way to deploy is to run the GitHub Actions workflow first, then stop nginx and initialize Let's Encrypt certificates as described in Initialize SSL Certificates. This approach requires almost no manual steps.


Prerequisites

  • VPS with Ubuntu 22.04+ (OVHcloud, Linode, Hetzner, etc.)
  • Domain name pointing to your VPS IP (e.g., demo.instacrud.it)
  • Docker and Docker Compose installed

Install Docker

curl -fsSL https://get.docker.com | sh
sudo usermod -aG docker $USER

Project Structure

The VPS deployment files are included in the repository:

instacrud/
├── docker-compose.vps.yml # VPS/ngrok orchestration
├── .env.vps.example # Environment template
├── backend/
│ └── Dockerfile # Shared (local + VPS)
├── frontend/
│ ├── Dockerfile # Local dev
│ └── Dockerfile.vps # VPS production (with PM2)
├── nginx/
│ ├── Dockerfile # Multi-mode nginx
│ ├── ssl.conf.template # SSL config (uses DOMAIN env var)
│ ├── nossl.conf # HTTP-only config (for ngrok)
│ ├── docker-entrypoint.sh # Config selector based on SSL_ENABLED
│ └── init-letsencrypt.sh # Let's Encrypt initialization
└── data/
└── certbot/ # Created on first run
├── conf/
└── www/

Environment Variables

Add these to /etc/environment on your VPS:

# Deployment mode (SSL for VPS with Let's Encrypt)
SSL_ENABLED="true"
DOMAIN="demo.instacrud.it"
BASE_URL="https://demo.instacrud.it"
ADMIN_EMAIL="admin@yourdomain.com" # Legacy: Let's Encrypt no longer sends expiration notices

# Required secrets
MONGO_PASSWORD="your-secure-mongodb-password"
SECRET_KEY="your-jwt-secret-key"

# OAuth (at least one recommended)
GOOGLE_CLIENT_ID="your-google-client-id"
GOOGLE_CLIENT_SECRET="your-google-client-secret"

MS_CLIENT_ID="your-microsoft-client-id"
MS_CLIENT_SECRET="your-microsoft-client-secret"
MS_TENANT_ID="common"

# Cloudflare Turnstile (anti-bot)
TURNSTILE_SITE_KEY="your-turnstile-site-key"
TURNSTILE_SECRET_KEY="your-turnstile-secret-key"

# Optional
EMAIL_ENABLED="true"
EMAIL_DRIVER="brevo"
BREVO_API_KEY="your-brevo-api-key"
DEEP_INFRA_KEY="your-deepinfra-key"
OPEN_REGISTRATION="false"

See .env.vps.example for a complete template.

Logout and login again for changes to take effect.


Deployment Steps

1. Clone Repository

cd /opt
git clone https://github.com/esng-one/instacrud.git instacrud
cd instacrud

2. Initialize SSL Certificates

First, edit nginx/init-letsencrypt.sh and set staging=1 to test with Let's Encrypt staging servers (avoids rate limits during testing):

staging=1  # Set to 1 for testing, 0 for production

Then run the script:

chmod +x nginx/init-letsencrypt.sh
./nginx/init-letsencrypt.sh
Important

You MUST delete ./data/certbot before switching from staging to production. If you skip this step, certbot will create certificates in a new directory with a -0001 suffix, causing nginx to fail because it looks for certificates in the original path.

Once everything works:

  1. Delete the certbot data: sudo rm -rf ./data/certbot
  2. Set staging=0 in nginx/init-letsencrypt.sh
  3. Run the script again: ./nginx/init-letsencrypt.sh

3. Start All Services

# Use --profile ssl to include certbot for automatic certificate renewal
docker compose -f docker-compose.vps.yml --profile ssl up -d

4. Initialize Database

Create root admin user:

docker compose -f docker-compose.vps.yml exec -it backend poetry run python -m init.init

This will prompt for admin email, password, and name.

Initialize AI models and tiers:

docker compose -f docker-compose.vps.yml exec backend poetry run python -m init.init_ai_models

Load sample data (optional):

warning

Do not run this in production. This creates a demo admin user with a known password (admin@test.org / admin123).

docker compose -f docker-compose.vps.yml exec backend poetry run python -m init.init_mock_db

Configuration Files

docker-compose.vps.yml

Key services:

  • mongo — MongoDB 8 with persistent volume
  • backend — FastAPI on port 8000 (internal)
  • frontend — Next.js on port 3000 (internal)
  • nginx — Reverse proxy on ports 80/443
  • certbot — SSL certificate management

nginx/default.conf

Routes:

PathDestination
/api/*backend:8000
/signin, /signupbackend:8000
/oauth/*backend:8000
/docs, /openapi.jsonbackend:8000
/_next/static/*frontend:3000 (cached)
/*frontend:3000

Maintenance

View Logs

docker compose -f docker-compose.vps.yml logs -f backend
docker compose -f docker-compose.vps.yml logs -f frontend
docker compose -f docker-compose.vps.yml logs -f nginx

Update Application

cd /opt/instacrud
git pull
docker compose -f docker-compose.vps.yml build
docker compose -f docker-compose.vps.yml up -d

Certificate Renewal

Certbot automatically renews certificates. To manually renew:

docker compose -f docker-compose.vps.yml run --rm certbot renew
docker compose -f docker-compose.vps.yml exec nginx nginx -s reload

Verify certificate validity:

# Check the certificate served by nginx
echo | openssl s_client -connect demo.instacrud.it:443 2>/dev/null | openssl x509 -noout -dates -issuer

# Check certificate files on disk
sudo openssl x509 -in ./data/certbot/conf/archive/demo.instacrud.it/fullchain1.pem -noout -dates -issuer

# Test renewal (dry run, does not actually renew)
docker compose -f docker-compose.vps.yml run --rm certbot renew --dry-run

A valid Let's Encrypt certificate will show issuer=...Let's Encrypt... and notAfter ~90 days from issuance.

Connect to MongoDB with a Client

You can connect to the MongoDB instance from your local machine using a GUI client (MongoDB Compass, Studio 3T, etc.) via an SSH tunnel.

1. Open an SSH tunnel from your local machine:

ssh -L 27017:127.0.0.1:27017 your-user@demo.instacrud.it

Keep this terminal open — it maintains the tunnel.

2. Connect your client using:

mongodb://instacrud:<MONGO_PASSWORD>@localhost:27017/

This works because the mongo service binds port 27017 to 127.0.0.1 on the VPS. The SSH tunnel forwards your local port 27017 to the VPS localhost, so no database port is exposed to the internet.

Database Backup

docker compose -f docker-compose.vps.yml exec mongo mongodump \
-u instacrud -p "$MONGO_PASSWORD" --authenticationDatabase admin \
--out /data/db/backup

Troubleshooting

Check service status

docker compose -f docker-compose.vps.yml ps

Verify nginx config

docker compose -f docker-compose.vps.yml exec nginx nginx -t

Test backend connectivity

curl https://your-domain.com/api/v1/heartbeat

A 401 Unauthorized with {"detail":"Authorization token required"} is a positive result — it means nginx is routing to the backend correctly (the endpoint requires authentication).


CI/CD with GitHub Actions

The repository includes a GitHub Actions workflow (.github/workflows/vps.yml) for automated deployments.

Trigger

  • Release tags only — Runs on full semantic version tags (1.0.0, 2.1.3) but ignores pre-releases (1.0.0-rc1, 1.0.0-beta)
  • Manual trigger — Can also be triggered manually via workflow_dispatch

Smart Installation

The workflow automatically installs dependencies only if they're not already present:

  • Git — Installs if not found
  • Docker — Installs via official script if not found
  • Docker Compose plugin — Installs if not found

Repository Handling

  • Existing installation — If /opt/instacrud exists, fetches and checks out the tag
  • Fresh installation — If not exists, clones the repository and checks out the tag

GitHub Secrets

Configure these secrets in your repository settings (Settings → Secrets and variables → Actions → Secrets):

SecretRequiredDescription
VPS_HOSTVPS IP address or hostname
VPS_USERNAMESSH username
VPS_PASSWORDSSH password
VPS_PORTSSH port (defaults to 22)
GH_PATGitHub Personal Access Token with repo scope (create one)
SSL_ENABLEDSet to true for Prod-ready VPS with SSL (defaults to false)
DOMAINDomain name without protocol (e.g., demo.instacrud.it)
ADMIN_EMAILLegacy: Let's Encrypt no longer sends expiration notices
MONGO_PASSWORDMongoDB password
SECRET_KEYJWT secret key
GOOGLE_CLIENT_IDGoogle OAuth client ID
GOOGLE_CLIENT_SECRETGoogle OAuth secret
MS_CLIENT_IDMicrosoft OAuth client ID
MS_CLIENT_SECRETMicrosoft OAuth secret
MS_TENANT_IDMicrosoft tenant ID
TURNSTILE_SITE_KEYCloudflare Turnstile site key
TURNSTILE_SECRET_KEYCloudflare Turnstile secret
EMAIL_ENABLEDEnable email (true/false)
EMAIL_DRIVEREmail driver (e.g., brevo)
BREVO_API_KEYBrevo API key
DEEP_INFRA_KEYDeepInfra API key for AI features
OPEN_REGISTRATIONAllow open registration (true/false)

Triggering a Deployment

Create and push a release tag:

git tag 1.0.0
git push origin 1.0.0

The workflow will automatically deploy to your VPS.


References


Summary

This VPS deployment provides:

  • Automatic HTTPS with Let's Encrypt
  • nginx reverse proxy for routing
  • Containerized MongoDB with persistent storage
  • Automatic certificate renewal
  • Production-ready logging configuration
  • Environment-based secrets management