Back to Docs
Installation Guide

Grid — Installation & Operations Guide

Complete guide to install, update, manage, and troubleshoot your Grid instance.

System Requirements

RequirementMinimumRecommended
OSUbuntu 20.04 LTSUbuntu 22.04 / 24.04 LTS
CPU2 vCPUs4 vCPUs
RAM2 GB4 GB+
Disk20 GB40 GB+ SSD
Ports80, 44380, 443, 8090

Software dependencies (Docker, Python 3, Caddy, Git) are installed automatically by the installer.

Fresh Installation

One-Command Install

SSH into your server as root and run:

curl -fsSL https://raw.githubusercontent.com/SMSLYCLOUD/smsly-hosting/main/install.sh -o /tmp/install.sh
sudo bash /tmp/install.sh
Important: Do NOT pipe directly from curl. The installer requires interactive input unless you pre-seed SSL env vars.

What Happens During Installation

1

Pre-flight

Checks OS, root access, available resources

2

Dependencies

Installs Docker, Python, system packages. Stops conflicting services

3

Configuration

Generates secrets: Django SECRET_KEY, Fernet key, DB/Redis passwords, HMAC gateway secret

4

Deployment

Builds and starts all Docker containers

5

Database

Waits for PostgreSQL, syncs passwords, runs Django migrations

6

Admin User

Creates admin superuser (credentials saved to /opt/smsly-hosting/.credentials)

7

Reverse Proxy

Installs Caddy for HTTP or HTTPS with auto-SSL

8

Verification

Runs health checks, prints container status, shows access URL

Installation Output

When the installer finishes, it prints a summary like this:

═══════════════════════════════════════════
  Grid Installation Complete!
═══════════════════════════════════════════
  Access URL: http://203.0.113.42
  Admin: admin
  Password: /opt/smsly-hosting/.credentials
═══════════════════════════════════════════

The Access URL is always http://YOUR_SERVER_IP for a fresh install — never HTTPS. See "Accessing the Dashboard" below for why.

Deployment Modes

The installer offers two modes. Choose during the interactive prompts. You can switch from IP mode to SSL mode at any time through the Settings UI.

IP Mode (Quick Start)

  • • Access via http://<IP>
  • • No domain required
  • • No SSL certificate
  • • Best for testing / evaluation
  • • Select option 1 during install

SSL Mode (Production) ✦

  • • Access via https://your-domain.com
  • • Auto Let's Encrypt SSL via Caddy
  • • Requires DNS A record pointing to your server
  • • Ports 80 + 443 must be publicly reachable
  • • Select option 2 during install
Note about HTTPS in IP mode: When no domain is configured, the server only binds port 80 (HTTP). Port 443 has a self-signed certificate that redirects to HTTP. Browsers will show a "Your connection is not private" warning if you manually type https://<IP>. Click "Advanced" → "Proceed" to be redirected to HTTP. Always use plain HTTP in IP mode to avoid this warning entirely.

Accessing the Dashboard

IP Mode (No Domain)

  1. Open your browser and go to http://YOUR_SERVER_IP (e.g. http://203.0.113.42)
  2. Do not use https:// — there is no valid certificate in IP mode
  3. If you accidentally visit https://, you will see a "Your connection is not private" warning. Click AdvancedProceed to site. This redirects you to HTTP automatically
  4. Log in with username admin and the password stored in /opt/smsly-hosting/.credentials

SSL Mode (With Domain)

  1. Ensure your domain has an A record pointing to your server IP
  2. Open your browser and go to https://your-domain.com
  3. Caddy automatically provisions a Let's Encrypt certificate on the first visit (may take 5-10 seconds the first time)
  4. Subsequent visits are instant with a valid, browser-trusted certificate

Why HTTP in IP Mode?

Let's Encrypt cannot issue certificates for raw IP addresses. The self-signed fallback certificate exists on port 443 only to redirect HTTPS traffic back to HTTP. Browsers warn on self-signed certificates before following the redirect. For a zero-warning experience, always access via HTTP when no domain is configured.

Already set up a domain? Go to Settings → Domain & SSL in the dashboard, enter your domain, toggle SSL on, and save. Caddy will automatically provision a Let's Encrypt certificate. No SSH required.

Domain & SSL Setup

Important: You do not need to SSH into the server to set up a domain. Everything is configurable from the dashboard under Settings → Domain & SSL.

How It Works

The system uses Caddy as its reverse proxy and TLS terminator. Caddy automatically provisions and renews Let's Encrypt certificates. The backend generates the Caddyfile dynamically based on your configuration in the database (PlatformConfig model) and applies it without downtime.

Step 1: DNS Setup

Before configuring SSL, your domain must resolve to your server:

Record TypeNameValue
A@YOUR_SERVER_IP

Verify propagation:

dig +short your-domain.com
# Should return your server IP
SSL will fail if DNS is not propagated. Caddy's ACME challenge (Let's Encrypt) must be able to reach your server on port 80. If DNS is wrong, the certificate cannot be issued.

Step 2: Configure via Dashboard

  1. Navigate to Settings → Domain & SSL
  2. Enter your domain (e.g. grid.your-domain.com)
  3. Toggle SSL Enabled ON
  4. Enter the server public IP (usually pre-filled)
  5. Click Save & Apply

After saving, the backend:

  1. Updates the PlatformConfig model in the database
  2. Regenerates the Caddyfile with your domain
  3. Reloads Caddy with zero downtime
  4. Syncs the domain back to the .env file (for future updates)
  5. Updates ALLOWED_HOSTS, CSRF_TRUSTED_ORIGINS, CORS_ALLOWED_ORIGINS, and SITE_URL at runtime

Step 3: Access via HTTPS

Visit https://your-domain.com. The first visit may take 5-10 seconds as Caddy obtains the Let's Encrypt certificate on-demand. Subsequent visits are instant.

Tip: The first visitor triggers certificate issuance. If the certificate doesn't appear after 30 seconds, check Caddy logs via docker logs smsly-hosting-caddy-1 on the server.

What the Backend Updates Automatically

When you save domain config via the dashboard, the backend runtime patches all the following Django settings (no restart needed):

SettingSourceUpdated
ALLOWED_HOSTSpatching.pyRuntime + DB
CSRF_TRUSTED_ORIGINSpatching.pyRuntime
CORS_ALLOWED_ORIGINSpatching.pyRuntime
SITE_URLpatching.pyRuntime
DOMAIN= (in .env)signals.pyFile (if writable)
Caddyfilecaddy_manager.pyFile + Reload

How .env Gets Updated

When the PlatformConfig model is saved (via the UI or API), a Django post_save signal fires (signals.py). This signal attempts to write the new DOMAIN, USE_SSL, SITE_URL, CSRF_TRUSTED_ORIGINS, and CORS_ALLOWED_ORIGINS values back to the .env file on the host.

The backend container runs as user smsly (UID 1000). The installer sets .env permissions to 664 with group ownership by GID 1000, so the container can write to it. If the permissions are wrong (e.g. 600 or 644 owned by root), the signal logs a warning and skips the file update — the domain still works because the database is the source of truth, but a future --update run would not pick up the new domain.

To fix this manually if it occurs:

sudo chown root:1000 /opt/smsly-hosting/.env
sudo chmod 664 /opt/smsly-hosting/.env

Switching from IP Mode to SSL Mode

  1. Create an A record for your domain → your server IP
  2. Wait for DNS propagation (verify with dig +short your-domain.com)
  3. Go to Settings → Domain & SSL
  4. Enter your domain, toggle SSL ON, click Save & Apply
  5. Access via https://your-domain.com

Removing a Domain (Revert to IP Mode)

  1. Go to Settings → Domain & SSL
  2. Clear the domain field, toggle SSL OFF
  3. Click Save & Apply
  4. Access via http://<IP>

Wildcard Subdomains

If you enable Wildcard Subdomains, Caddy provisions a *.your-domain.com certificate via Cloudflare's DNS-01 challenge. This requires:

  1. Setting Cloudflare API Token (DNS: Edit zone DNS permission) in Settings → Domain & SSL
  2. Your domain must be on Cloudflare DNS
  3. The wildcard cert covers all *.your-domain.com subdomains automatically

Common Edge Cases

"Your connection is not private" / ERR_CERT_AUTHORITY_INVALID

You accessed https://<IP> in IP mode (no domain). The server has a self-signed certificate on port 443 that redirects to HTTP, but browsers warn before following the redirect.

Fix: Use http://<IP> instead. If you already set up a domain, use https://your-domain.com. If you clicked through the warning, you are automatically redirected to HTTP.

SSL Certificate Not Issued (HTTPS shows self-signed after domain config)

This usually means Caddy's on-demand TLS "ask" endpoint rejected the domain, or DNS hasn't propagated.

Diagnose from the server:

# 1. Check the ask endpoint (must return 200)
curl -s -w "
HTTP %{http_code}
" \
  "http://localhost:8090/api/v1/services/check-domain/?domain=your-domain.com"

# 2. Check Caddy logs for ACME errors
docker logs smsly-hosting-caddy-1 --tail 50

# 3. Force a Caddy reload to retry cert issuance
docker exec smsly-hosting-caddy-1 caddy reload --config /etc/caddy/Caddyfile

If the ask endpoint returns 200, Caddy should get the cert on the next HTTPS visit. If 404, the domain is not in the PlatformConfig database — re-save it via Settings → Domain & SSL.

Gateway Timeout After Reboot

On first boot after a restart, the backend runs database migrations and waits for PostgreSQL to be healthy. This can take 1-3 minutes.

Check progress:

docker compose -f docker-compose.prod.yml logs backend --tail 30

You will see messages like Waiting for database... followed by Starting gunicorn. The dashboard becomes available once the backend is healthy.

Frontend API Calls to HTTP Instead of HTTPS

The frontend uses relative API paths (/api/v1/...), so the scheme (HTTP vs HTTPS) matches whatever the browser is using. If you access via http://<IP>, API calls use HTTP. If via https://domain, they use HTTPS.

If you see mixed-content errors or API calls going to the wrong scheme, ensure you're accessing via the correct URL for your mode.

Domain Saved via UI but .env Not Updated

The backend container (user smsly, UID 1000) needs write permission on the host .env file. If the file is owned by root with 644, the write fails with PermissionError.

Check and fix:

ls -la /opt/smsly-hosting/.env
# If owned by root:root, fix with:
sudo chown root:1000 /opt/smsly-hosting/.env
sudo chmod 664 /opt/smsly-hosting/.env

The domain still works because it's stored in the database. The .env is only needed for future --update runs and container restarts.

"403 Forbidden" or "CSRF token missing" After Domain Change

The patch_runtime_settings() function should update CSRF_TRUSTED_ORIGINS and CORS_ALLOWED_ORIGINS automatically when you save domain config. If these didn't update, re-save the domain config via Settings → Domain & SSL.

If the issue persists, the backend may need a restart: docker compose -f docker-compose.prod.yml restart backend

Updating Grid

From the Terminal

Full Update (Frontend + Backend)

cd /opt/smsly-hosting
sudo bash install.sh --update

Frontend Only (1-2 min, no backend downtime)

cd /opt/smsly-hosting
sudo bash install.sh --update-frontend

Backend Only (includes migrations)

cd /opt/smsly-hosting
sudo bash install.sh --update-backend

Rollback on Failure

If an update fails, the installer automatically:

  1. Stops new containers
  2. Restores the previous .env backup
  3. Pops the git stash (rolls back code)

Manual rollback:

cd /opt/smsly-hosting
git log --oneline -n 5           # Find the previous commit
git checkout <commit-hash>       # Roll back
docker compose -f docker-compose.prod.yml up -d --build

From the Dashboard

Admins can trigger updates from Settings → System → Update Software.

Managing Services

View Container Status

cd /opt/smsly-hosting
docker compose -f docker-compose.prod.yml ps

View Logs

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

# Specific service
docker compose -f docker-compose.prod.yml logs -f backend
docker compose -f docker-compose.prod.yml logs -f frontend
docker compose -f docker-compose.prod.yml logs -f caddy

Restart Services

# Restart everything
docker compose -f docker-compose.prod.yml restart

# Restart specific service
docker compose -f docker-compose.prod.yml restart backend
docker compose -f docker-compose.prod.yml restart caddy

Health Check

curl http://localhost:8090/health

Container Map

ServicePortPurpose
caddy80 / 443Reverse proxy, TLS termination, Let's Encrypt
backend8000Django API (Gunicorn + Uvicorn)
frontend3000Next.js SSR
nginx8090Internal routing (behind Caddy)
db5432PostgreSQL 16
redis6379Cache + Celery broker
celeryBackground task worker
celery-beatPeriodic task scheduler
rabbitmq5672Message broker for Celery
socket-proxy2375Secured Docker API proxy

Architecture note: All external traffic enters through Caddy (ports 80/443). Caddy terminates TLS and proxies to nginx (port 80 internal), which routes to the appropriate backend or frontend service. The stack does not expose backend/frontend ports directly to the internet.

Database Operations

Backup

cd /opt/smsly-hosting
docker compose -f docker-compose.prod.yml exec db \
  pg_dump -U smsly_admin smsly_hosting | gzip > backup_$(date +%Y%m%d).sql.gz

Automated Backups

Add to root crontab (crontab -e):

# Daily at 2 AM
0 2 * * * cd /opt/smsly-hosting && docker compose -f docker-compose.prod.yml exec -T db pg_dump -U smsly_admin smsly_hosting | gzip > backups/daily_$(date +\%Y\%m\%d).sql.gz

Restore from Backup

# Stop write-services
docker compose -f docker-compose.prod.yml stop backend celery celery-beat

# Restore
gunzip -c backup_20260211.sql.gz | \
  docker compose -f docker-compose.prod.yml exec -T db psql -U smsly_admin -d smsly_hosting

# Restart
docker compose -f docker-compose.prod.yml start backend celery celery-beat

Reset Admin Password

docker compose -f docker-compose.prod.yml exec backend python manage.py shell -c \
  "from django.contrib.auth import get_user_model; User = get_user_model(); u = User.objects.get(username='admin'); u.set_password('your_new_password'); u.save(); print('Done.')"

Troubleshooting

Dashboard Not Loading (blank page or 502)
Check that all containers are running: docker compose -f docker-compose.prod.yml ps. Wait for backend migrations to finish (may take 1-3 min after reboot). Check nginx on port 8090: curl http://localhost:8090/health. Verify firewall allows ports 80/443: ufw status.
ERR_CERT_AUTHORITY_INVALID
You are likely accessing https://SERVER_IP in IP mode. Use http://SERVER_IP instead. If you have configured a domain, ensure DNS resolves correctly (dig +short your-domain.com) and the domain is saved in Settings → Domain & SSL.
Gateway Timeout (504)
The backend is still starting up. Run docker compose -f docker-compose.prod.yml logs backend --tail 30 to check progress. Common causes: database migrations, waiting for PostgreSQL health check, or slow build on first boot. Wait 2-3 minutes.
Caddy SSL Error — Certificate Not Issued
Verify DNS resolves (host your-domain.com), check Caddy logs (docker logs smsly-hosting-caddy-1), ensure ports 80/443 are open from outside (curl -v http://your-domain.com/.well-known/acme-challenge/check should not timeout). The ask endpoint must return 200 for your domain: curl -s "http://localhost:8090/api/v1/services/check-domain/?domain=your-domain.com".
Database Connection Error
Check backend logs (docker compose -f docker-compose.prod.yml logs backend --tail 20). Verify POSTGRES_PASSWORD in .env matches what was set during install. Re-sync with: sudo bash install.sh --update.
Build Fails During Update
Check disk space (df -h), clean Docker cache (docker system prune -f), re-run the update. If the issue persists, the error may be in the build logs at /var/log/smsly-install.log.
Container Keeps Restarting (CrashLoop)
Run docker compose -f docker-compose.prod.yml ps to identify the unhealthy container, then check its logs: docker compose -f docker-compose.prod.yml logs --tail=50 <service>. Common causes: database not reachable, port conflicts, or missing environment variables.
403 Forbidden or CSRF Validation Failed
The backend's CSRF_TRUSTED_ORIGINS may not include your current origin. Re-save the domain config via Settings → Domain & SSL to trigger the runtime patch. If that fails, restart the backend: docker compose -f docker-compose.prod.yml restart backend.
After Reboot, Everything Is Down
Containers with restart: unless-stopped will auto-start after a Docker daemon restart. Give it 2-3 minutes for the stack to fully initialize. Run docker compose -f docker-compose.prod.yml ps to check status. If containers are not running, start manually: docker compose -f docker-compose.prod.yml up -d.
How Do I Force Caddy to Renew / Retry a Certificate?
Run: docker exec smsly-hosting-caddy-1 caddy reload --config /etc/caddy/Caddyfile. This reloads the config and triggers ACME retries for any domains without valid certificates.

Security Hardening

Post-Install Checklist

Change the admin password (recommended)
Verify DEBUG=False in .env
Configure ALLOWED_HOSTS to only your domain
Enable SSL mode for production
Set up UFW firewall (ports 80, 443, SSH only)

Firewall Setup (UFW)

ufw default deny incoming
ufw default allow outgoing
ufw allow ssh
ufw allow 80/tcp
ufw allow 443/tcp
ufw enable

Port 8090 (nginx direct access) should not be exposed externally — it is bound to 127.0.0.1 by default. Ports 5432 (PostgreSQL), 6379 (Redis), and other internal ports are only accessible within the Docker network and should not be exposed to the internet.

.env File Permissions

FilePurposePermissions
/opt/smsly-hosting/.envAll secrets & config (database passwords, API keys)664 (root:1000)
/opt/smsly-hosting/.credentialsAdmin login info600
/opt/smsly-hosting/caddy-config/Caddy configuration & certificates775 (1000:1000)

The .env file needs 664 permissions with group ownership by GID 1000 because the backend Docker container runs as user smsly (UID 1000). This allows the domain-config signal to persist domain changes from the UI back to .env without requiring SSH access.

Architecture Summary

Understanding the request flow helps diagnose issues:

Browser → https://your-domain.com
  │
  ▼
Caddy (port 443)
  ├─ Terminates TLS (Let's Encrypt cert)
  ├─ On-demand TLS: asks backend "is this domain allowed?"
  │    → GET /api/v1/services/check-domain/?domain=your-domain.com
  │    → 200 OK = proceed, 404 = reject
  └─ Proxies to → nginx (port 80 internal)
                      │
                      ├─ /api/*      → backend (port 8000)
                      ├─ /ws/*       → backend (WebSocket)
                      ├─ /admin/*    → backend
                      ├─ /static/*   → served directly
                      └─ /*          → frontend (port 3000)

Key insight: Caddy's on-demand TLS "ask" endpoint at /api/v1/services/check-domain/ is the gatekeeper for certificate issuance. If it returns 404, Caddy will not obtain a Let's Encrypt certificate for that domain. The endpoint checks (in order): PlatformConfig primary domain, managed servers, service public domains, verified custom domains, and addon domains.