Skip to main content
This guide is for teams deploying Ringlyra onto a Google Cloud Compute Engine (GCE) VM that already has nginx and other applications running. Instead of using Ringlyra’s built-in nginx container, you will add a virtual-host block to your existing nginx and point your domain www.ringlyra.com at it.

Architecture overview

Internet
  β”‚
  β–Ό
GCE VM (your existing nginx on port 80 / 443)
  β”œβ”€β”€ www.ringlyra.com  ──►  nginx virtual host (this guide)
  β”‚       β”œβ”€β”€ /api/v1/  ──►  localhost:8000  (Ringlyra API container)
  β”‚       β”œβ”€β”€ /voice-audio/  ──►  localhost:9000  (MinIO container)
  β”‚       └── /         ──►  localhost:3010  (Ringlyra UI container)
  β”‚
  └── your-other-apps  ──►  their own virtual hosts (unchanged)
Ringlyra runs via Docker Compose without its own nginx container β€” the remote profile is intentionally skipped.

Step 1 β€” GCP firewall rules

Open the required ports in your GCP project firewall. Run these from Cloud Shell or your local gcloud CLI:
# HTTP + HTTPS for web traffic
gcloud compute firewall-rules create ringlyra-web \
  --direction=INGRESS --action=ALLOW \
  --rules=tcp:80,tcp:443 \
  --target-tags=ringlyra-server

# WebRTC (TURN server for voice calls)
gcloud compute firewall-rules create ringlyra-webrtc \
  --direction=INGRESS --action=ALLOW \
  --rules=tcp:3478,tcp:5349,udp:3478,udp:5349,udp:49152-49200 \
  --target-tags=ringlyra-server
Then add the ringlyra-server network tag to your VM:
gcloud compute instances add-tags YOUR_INSTANCE_NAME \
  --zone=YOUR_ZONE \
  --tags=ringlyra-server

Reserve a static external IP

A static IP prevents the VM’s address from changing on restart, which would break DNS.
# Reserve a static IP (if you don't already have one)
gcloud compute addresses create ringlyra-ip --region=YOUR_REGION

# Assign it to your instance
gcloud compute instances delete-access-config YOUR_INSTANCE_NAME \
  --access-config-name "External NAT" --zone=YOUR_ZONE

gcloud compute instances add-access-config YOUR_INSTANCE_NAME \
  --access-config-name "External NAT" \
  --address=$(gcloud compute addresses describe ringlyra-ip \
    --region=YOUR_REGION --format='get(address)') \
  --zone=YOUR_ZONE
Note the IP β€” you need it for DNS in the next step.

Step 2 β€” DNS

Log in to your domain registrar and add two A records pointing to your GCE VM’s static IP:
TypeNameValueTTL
A@ (root)YOUR_VM_IP300
AwwwYOUR_VM_IP300
Verify propagation before continuing:
nslookup www.ringlyra.com
# Expected: YOUR_VM_IP

Step 3 β€” Deploy Ringlyra containers

SSH into your GCE VM.

3a β€” Clone the repo and create the environment file

cd /opt   # or wherever you keep applications
git clone https://github.com/MohitMaliFtechiz/RingLyra.git ringlyra
cd ringlyra
Copy the example env file and fill in your values:
cp api/.env.example .env
nano .env
Minimum required values for a production deployment:
ENVIRONMENT="production"
SERVER_IP="YOUR_VM_IP"
PUBLIC_HOST="www.ringlyra.com"
PUBLIC_BASE_URL="https://www.ringlyra.com"
BACKEND_API_ENDPOINT="https://www.ringlyra.com"
UI_APP_URL="https://www.ringlyra.com"
MINIO_PUBLIC_ENDPOINT="https://www.ringlyra.com"

# Generate with: openssl rand -hex 32
OSS_JWT_SECRET="REPLACE_WITH_STRONG_SECRET"

# WebRTC TURN relay for browser voice calls
TURN_HOST="www.ringlyra.com"
# API container uses Docker DNS to reach the built-in coturn service
TURN_INTERNAL_HOST="coturn"
TURN_SECRET="REPLACE_WITH_TURN_SECRET"
# Recommended while validating remote audio paths
FORCE_TURN_RELAY="true"

DATABASE_URL="postgresql+asyncpg://postgres:postgres@localhost:5432/postgres"
REDIS_URL="redis://:redissecret@localhost:6379"

MINIO_ENDPOINT=localhost:9000
MINIO_ACCESS_KEY=minioadmin
MINIO_SECRET_KEY=minioadmin
MINIO_BUCKET=voice-audio

# SMTP (for OTP / password reset emails)
SMTP_HOST=smtppro.zoho.in
SMTP_PORT=465
SMTP_USER=notifications@ftechiz.com
SMTP_PASSWORD=YOUR_SMTP_PASSWORD
SMTP_FROM_EMAIL=notifications@ftechiz.com
SMTP_FROM_NAME=Ringlyra

3b β€” Start the stack (without the built-in nginx)

The remote Docker Compose profile starts Ringlyra’s own nginx container, which conflicts with your existing nginx on ports 80/443. Use the deploy script instead; it starts the app services and starts coturn automatically when TURN_HOST and TURN_SECRET are present:
bash infra/deploy.sh --build
This starts the API on localhost:8000, the UI on localhost:3010, MinIO on localhost:9000, and coturn on the TURN ports from Step 1. Verify all containers are healthy:
docker compose ps
# postgres, redis, minio, api, ui, and coturn should show "healthy" or "running"

3c β€” Database migrations

infra/deploy.sh runs Alembic migrations automatically after the containers are healthy. If you start the services manually instead of using the deploy script, run:
docker compose exec api python -m alembic -c api/alembic.ini upgrade head

Step 4 β€” SSL certificate with Let’s Encrypt

Install Certbot on the VM (if not already installed):
sudo apt update && sudo apt install -y certbot python3-certbot-nginx
Stop your existing nginx temporarily so Certbot can bind to port 80 for the ACME challenge:
sudo systemctl stop nginx
Obtain the certificate:
sudo certbot certonly --standalone \
  -d ringlyra.com \
  -d www.ringlyra.com \
  --email YOUR_EMAIL@example.com \
  --agree-tos --non-interactive
Certificates are stored in /etc/letsencrypt/live/ringlyra.com/. Restart nginx:
sudo systemctl start nginx

Step 5 β€” nginx virtual host

Create a new site config for Ringlyra. This sits alongside your existing sites and does not touch them.
sudo nano /etc/nginx/sites-available/ringlyra
Paste the following (replace www.ringlyra.com / ringlyra.com if your domain differs):
# Redirect bare domain β†’ www
server {
    listen 80;
    listen 443 ssl;
    server_name ringlyra.com;

    ssl_certificate     /etc/letsencrypt/live/ringlyra.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/ringlyra.com/privkey.pem;

    return 301 https://www.ringlyra.com$request_uri;
}

# Redirect HTTP β†’ HTTPS
server {
    listen 80;
    server_name www.ringlyra.com;
    return 301 https://www.ringlyra.com$request_uri;
}

# Main HTTPS virtual host
server {
    listen 443 ssl;
    server_name www.ringlyra.com;

    ssl_certificate     /etc/letsencrypt/live/ringlyra.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/ringlyra.com/privkey.pem;
    ssl_protocols       TLSv1.2 TLSv1.3;
    ssl_prefer_server_ciphers on;

    # ── Backend API + WebSocket (audio streaming, signaling) ───────────────
    location /api/v1/ {
        proxy_pass http://localhost:8000;
        proxy_http_version 1.1;

        proxy_set_header Upgrade    $http_upgrade;
        proxy_set_header Connection "upgrade";
        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  https;

        # Long-lived WebSocket connections for voice calls
        proxy_read_timeout  3600s;
        proxy_send_timeout  3600s;
        proxy_buffering     off;
        client_max_body_size 100M;
    }

    # ── MinIO audio file storage ───────────────────────────────────────────
    location /voice-audio/ {
        proxy_pass http://localhost:9000/voice-audio/;
        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  https;

        proxy_buffering off;
        client_max_body_size 100M;
    }

    # ── Next.js UI (catch-all) ─────────────────────────────────────────────
    location / {
        proxy_pass http://localhost:3010;
        proxy_http_version 1.1;

        proxy_set_header Upgrade    $http_upgrade;
        proxy_set_header Connection "upgrade";
        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  https;

        # Rewrite internal MinIO URLs in API responses to use the public domain
        sub_filter 'http://localhost:9000/voice-audio/' 'https://www.ringlyra.com/voice-audio/';
        sub_filter_once off;
        sub_filter_types application/json text/html;
    }
}
Enable the site and reload nginx:
sudo ln -s /etc/nginx/sites-available/ringlyra /etc/nginx/sites-enabled/ringlyra
sudo nginx -t          # verify β€” must say "syntax is ok"
sudo systemctl reload nginx

Step 6 β€” Verify the deployment

# API health check
curl https://www.ringlyra.com/api/v1/health

# UI loads
curl -I https://www.ringlyra.com
# Expected: HTTP/2 200
Open https://www.ringlyra.com in your browser β€” you should see the Ringlyra dashboard with a valid SSL certificate.

Step 7 β€” Automatic certificate renewal

Certbot installs a renewal timer automatically, but it needs to reload nginx after renewing. Create a deploy hook:
sudo nano /etc/letsencrypt/renewal-hooks/deploy/ringlyra-reload.sh
#!/bin/bash
systemctl reload nginx
sudo chmod +x /etc/letsencrypt/renewal-hooks/deploy/ringlyra-reload.sh

# Test renewal dry-run
sudo certbot renew --dry-run

Step 8 β€” GitHub Actions auto-deploy

If you set up the GitHub Actions deploy workflow, add these secrets in your repo settings so the workflow can SSH into this VM and restart the containers:
SecretValue
GCP_PROJECT_IDYour GCP project ID
GCP_SA_KEYService account JSON key
GCP_REGIONVM region (e.g. us-central1)
AR_REPOSITORYArtifact Registry repo name
GCE_INSTANCE_NAMEVM instance name
GCE_ZONEVM zone (e.g. us-central1-a)
DEPLOY_PATH/opt/ringlyra (or wherever you cloned)
On every push to main, the workflow will build fresh Docker images, push them to Artifact Registry, SSH in via IAP, pull the images, and restart only the api and ui containers β€” leaving your existing nginx and other applications untouched.

Troubleshooting

502 Bad Gateway from nginx

The API or UI container is not running. Check:
docker compose ps
docker compose logs api --tail=50
docker compose logs ui  --tail=50

Voice calls connect but no audio

WebRTC relay ports may be blocked. Confirm the ringlyra-webrtc firewall rule is applied to your instance tag and that coturn is running:
docker compose ps coturn
docker compose logs coturn --tail=80
If coturn is not running, make sure .env includes TURN_HOST, TURN_INTERNAL_HOST, TURN_SECRET, and SERVER_IP, then re-run:
bash infra/deploy.sh --build
If calls connect but audio still fails, temporarily keep FORCE_TURN_RELAY=true so browser media uses relay candidates only while you verify the firewall and coturn logs.

Certificate renewal fails

Ensure port 80 is reachable from the internet (required for HTTP-01 ACME challenge) and that the ringlyra firewall rule is active:
gcloud compute firewall-rules describe ringlyra-web