System Requirements
| Requirement | Minimum | Recommended |
|---|---|---|
| OS | Ubuntu 20.04 LTS | Ubuntu 22.04 / 24.04 LTS |
| CPU | 2 vCPUs | 4 vCPUs |
| RAM | 2 GB | 4 GB+ |
| Disk | 20 GB | 40 GB+ SSD |
| Ports | 80, 443 | 80, 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.shWhat Happens During Installation
Pre-flight
Checks OS, root access, available resources
Dependencies
Installs Docker, Python, system packages. Stops conflicting services
Configuration
Generates secrets: Django SECRET_KEY, Fernet key, DB/Redis passwords, HMAC gateway secret
Deployment
Builds and starts all Docker containers
Database
Waits for PostgreSQL, syncs passwords, runs Django migrations
Admin User
Creates admin superuser (credentials saved to /opt/smsly-hosting/.credentials)
Reverse Proxy
Installs Caddy for HTTP or HTTPS with auto-SSL
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
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)
- Open your browser and go to
http://YOUR_SERVER_IP(e.g.http://203.0.113.42) - Do not use
https://— there is no valid certificate in IP mode - If you accidentally visit
https://, you will see a "Your connection is not private" warning. Click Advanced → Proceed to site. This redirects you to HTTP automatically - Log in with username
adminand the password stored in/opt/smsly-hosting/.credentials
SSL Mode (With Domain)
- Ensure your domain has an A record pointing to your server IP
- Open your browser and go to
https://your-domain.com - Caddy automatically provisions a Let's Encrypt certificate on the first visit (may take 5-10 seconds the first time)
- 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.
Domain & SSL Setup
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 Type | Name | Value |
|---|---|---|
| A | @ | YOUR_SERVER_IP |
Verify propagation:
dig +short your-domain.com
# Should return your server IPStep 2: Configure via Dashboard
- Navigate to Settings → Domain & SSL
- Enter your domain (e.g.
grid.your-domain.com) - Toggle SSL Enabled ON
- Enter the server public IP (usually pre-filled)
- Click Save & Apply
After saving, the backend:
- Updates the
PlatformConfigmodel in the database - Regenerates the Caddyfile with your domain
- Reloads Caddy with zero downtime
- Syncs the domain back to the
.envfile (for future updates) - Updates
ALLOWED_HOSTS,CSRF_TRUSTED_ORIGINS,CORS_ALLOWED_ORIGINS, andSITE_URLat 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.
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):
| Setting | Source | Updated |
|---|---|---|
| ALLOWED_HOSTS | patching.py | Runtime + DB |
| CSRF_TRUSTED_ORIGINS | patching.py | Runtime |
| CORS_ALLOWED_ORIGINS | patching.py | Runtime |
| SITE_URL | patching.py | Runtime |
| DOMAIN= (in .env) | signals.py | File (if writable) |
| Caddyfile | caddy_manager.py | File + 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/.envSwitching from IP Mode to SSL Mode
- Create an A record for your domain → your server IP
- Wait for DNS propagation (verify with
dig +short your-domain.com) - Go to Settings → Domain & SSL
- Enter your domain, toggle SSL ON, click Save & Apply
- Access via
https://your-domain.com
Removing a Domain (Revert to IP Mode)
- Go to Settings → Domain & SSL
- Clear the domain field, toggle SSL OFF
- Click Save & Apply
- 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:
- Setting Cloudflare API Token (DNS: Edit zone DNS permission) in Settings → Domain & SSL
- Your domain must be on Cloudflare DNS
- The wildcard cert covers all
*.your-domain.comsubdomains 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/CaddyfileIf 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 30You 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/.envThe 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 --updateFrontend Only (1-2 min, no backend downtime)
cd /opt/smsly-hosting
sudo bash install.sh --update-frontendBackend Only (includes migrations)
cd /opt/smsly-hosting
sudo bash install.sh --update-backendRollback on Failure
If an update fails, the installer automatically:
- Stops new containers
- Restores the previous
.envbackup - 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 --buildFrom 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 psView 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 caddyRestart 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 caddyHealth Check
curl http://localhost:8090/healthContainer Map
| Service | Port | Purpose |
|---|---|---|
| caddy | 80 / 443 | Reverse proxy, TLS termination, Let's Encrypt |
| backend | 8000 | Django API (Gunicorn + Uvicorn) |
| frontend | 3000 | Next.js SSR |
| nginx | 8090 | Internal routing (behind Caddy) |
| db | 5432 | PostgreSQL 16 |
| redis | 6379 | Cache + Celery broker |
| celery | — | Background task worker |
| celery-beat | — | Periodic task scheduler |
| rabbitmq | 5672 | Message broker for Celery |
| socket-proxy | 2375 | Secured 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.gzAutomated 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.gzRestore 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-beatReset 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)
ERR_CERT_AUTHORITY_INVALID
Gateway Timeout (504)
Caddy SSL Error — Certificate Not Issued
Database Connection Error
Build Fails During Update
Container Keeps Restarting (CrashLoop)
403 Forbidden or CSRF Validation Failed
After Reboot, Everything Is Down
How Do I Force Caddy to Renew / Retry a Certificate?
Security Hardening
Post-Install Checklist
Firewall Setup (UFW)
ufw default deny incoming
ufw default allow outgoing
ufw allow ssh
ufw allow 80/tcp
ufw allow 443/tcp
ufw enablePort 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
| File | Purpose | Permissions |
|---|---|---|
| /opt/smsly-hosting/.env | All secrets & config (database passwords, API keys) | 664 (root:1000) |
| /opt/smsly-hosting/.credentials | Admin login info | 600 |
| /opt/smsly-hosting/caddy-config/ | Caddy configuration & certificates | 775 (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.