Files
orion/docs/deployment/hetzner-server-setup.md
Samir Boulahtit bf5bb69409
Some checks failed
CI / ruff (push) Failing after 1m34s
CI / pytest (push) Failing after 1s
CI / architecture (push) Failing after 7s
CI / dependency-scanning (push) Successful in 30s
CI / audit (push) Failing after 7s
CI / docs (push) Has been skipped
docs(deployment): update server setup guide with wizard.lu domain
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-11 23:27:15 +01:00

541 lines
13 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Hetzner Cloud Server Setup
Complete step-by-step guide for deploying Wizamart on a Hetzner Cloud VPS.
!!! info "Server Details"
- **Provider**: Hetzner Cloud
- **OS**: Ubuntu 24.04.3 LTS (upgraded to 24.04.4 after updates)
- **Architecture**: aarch64 (ARM64)
- **IP**: `91.99.65.229`
- **IPv6**: `2a01:4f8:1c1a:b39c::1`
- **Disk**: 37 GB
- **RAM**: 4 GB
- **Auth**: SSH key (configured via Hetzner Console)
- **Setup date**: 2026-02-11
!!! success "Progress — 2026-02-11"
**Completed (Steps 112):**
- Non-root user `samir` with SSH key
- Server hardened (UFW firewall, SSH root login disabled, fail2ban)
- Docker 29.2.1 & Docker Compose 5.0.2 installed
- Gitea running at `http://91.99.65.229:3000` (user: `sboulahtit`, repo: `orion`)
- Repository cloned to `~/apps/orion`
- Production `.env` configured with generated secrets
- Full Docker stack deployed (API, PostgreSQL, Redis, Celery worker/beat, Flower)
- Database migrated (76 tables) and seeded (admin, platforms, CMS, email templates)
- API verified at `http://91.99.65.229:8001/docs` and `/admin/login`
**Remaining (Steps 1315):**
- [ ] DNS: Point domain A records to `91.99.65.229`
- [ ] Caddy reverse proxy with auto-SSL
- [ ] Gitea Actions runner for CI/CD
- [ ] Remove temporary firewall rules (ports 3000, 8001)
## Installed Software Versions
| Software | Version |
|---|---|
| Ubuntu | 24.04.4 LTS |
| Kernel | 6.8.0-100-generic (aarch64) |
| Docker | 29.2.1 |
| Docker Compose | 5.0.2 |
| PostgreSQL | 15 (container) |
| Redis | 7-alpine (container) |
| Python | 3.11-slim (container) |
| Gitea | latest (container) |
---
## Step 1: Initial Server Access
```bash
ssh root@91.99.65.229
```
## Step 2: Create Non-Root User
Create a dedicated user with sudo privileges and copy the SSH key:
```bash
# Create user
adduser samir
usermod -aG sudo samir
# Copy SSH keys to new user
rsync --archive --chown=samir:samir ~/.ssh /home/samir
```
Verify by connecting as the new user (from a **new terminal**):
```bash
ssh samir@91.99.65.229
```
## Step 3: System Update & Essential Packages
```bash
sudo apt update && sudo apt upgrade -y
sudo apt install -y \
curl \
git \
wget \
ufw \
fail2ban \
htop \
unzip \
make
```
Reboot if a kernel upgrade is pending:
```bash
sudo reboot
```
## Step 4: Firewall Configuration (UFW)
```bash
sudo ufw allow OpenSSH
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw enable
```
Verify:
```bash
sudo ufw status
```
Expected output:
```
Status: active
To Action From
-- ------ ----
OpenSSH ALLOW Anywhere
80/tcp ALLOW Anywhere
443/tcp ALLOW Anywhere
```
## Step 5: Harden SSH
!!! warning "Before doing this step"
Make sure you can SSH as `samir` from another terminal first!
If you lock yourself out, you'll need to use Hetzner's console rescue mode.
```bash
sudo sed -i 's/^#\?PermitRootLogin.*/PermitRootLogin no/' /etc/ssh/sshd_config
sudo sed -i 's/^#\?PasswordAuthentication.*/PasswordAuthentication no/' /etc/ssh/sshd_config
sudo systemctl restart ssh # Note: Ubuntu 24.04 uses 'ssh' not 'sshd'
```
## Step 6: Install Docker & Docker Compose
```bash
curl -fsSL https://get.docker.com | sh
sudo usermod -aG docker samir
```
Log out and back in for the group change:
```bash
exit
# Then: ssh samir@91.99.65.229
```
Verify:
```bash
docker --version
docker compose version
```
## Step 7: Gitea (Self-Hosted Git)
Create the Gitea directory and compose file:
```bash
mkdir -p ~/gitea && cd ~/gitea
```
Create `docker-compose.yml` with `nano ~/gitea/docker-compose.yml`:
```yaml
services:
gitea:
image: gitea/gitea:latest
container_name: gitea
restart: always
environment:
- USER_UID=1000
- USER_GID=1000
- GITEA__database__DB_TYPE=postgres
- GITEA__database__HOST=gitea-db:5432
- GITEA__database__NAME=gitea
- GITEA__database__USER=gitea
- GITEA__database__PASSWD=<GENERATED_PASSWORD>
- GITEA__server__ROOT_URL=http://91.99.65.229:3000/
- GITEA__server__SSH_DOMAIN=91.99.65.229
- GITEA__server__DOMAIN=91.99.65.229
- GITEA__actions__ENABLED=true
volumes:
- gitea-data:/data
- /etc/timezone:/etc/timezone:ro
- /etc/localtime:/etc/localtime:ro
ports:
- "3000:3000"
- "2222:22"
depends_on:
gitea-db:
condition: service_healthy
gitea-db:
image: postgres:15
container_name: gitea-db
restart: always
environment:
POSTGRES_DB: gitea
POSTGRES_USER: gitea
POSTGRES_PASSWORD: <GENERATED_PASSWORD>
volumes:
- gitea-db-data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U gitea"]
interval: 10s
timeout: 5s
retries: 5
volumes:
gitea-data:
gitea-db-data:
```
Generate the database password with `openssl rand -hex 16` and replace `<GENERATED_PASSWORD>` in both places.
Open the firewall for Gitea and start:
```bash
sudo ufw allow 3000/tcp
docker compose up -d
docker compose ps
```
Visit `http://91.99.65.229:3000` and complete the setup wizard. Create an admin account (e.g. `sboulahtit`).
Then create a repository (e.g. `orion`).
## Step 8: Push Repository to Gitea
From your **local machine**:
```bash
cd /home/samir/Documents/PycharmProjects/letzshop-product-import
git remote add gitea http://91.99.65.229:3000/sboulahtit/orion.git
git push gitea master
```
## Step 9: Clone Repository on Server
```bash
mkdir -p ~/apps
cd ~/apps
git clone http://localhost:3000/sboulahtit/orion.git
cd orion
```
## Step 10: Configure Production Environment
```bash
cp .env.example .env
nano .env
```
### Critical Production Values
Generate secrets:
```bash
openssl rand -hex 32 # For JWT_SECRET_KEY
openssl rand -hex 16 # For database password
```
| Variable | How to Generate / What to Set |
|---|---|
| `DEBUG` | `False` |
| `DATABASE_URL` | `postgresql://wizamart_user:YOUR_DB_PW@db:5432/wizamart_db` |
| `JWT_SECRET_KEY` | Output of `openssl rand -hex 32` |
| `ADMIN_PASSWORD` | Strong password |
| `USE_CELERY` | `true` |
| `REDIS_URL` | `redis://redis:6379/0` |
| `STRIPE_SECRET_KEY` | Your Stripe secret key (configure later) |
| `STRIPE_PUBLISHABLE_KEY` | Your Stripe publishable key (configure later) |
| `STRIPE_WEBHOOK_SECRET` | Your Stripe webhook secret (configure later) |
| `STORAGE_BACKEND` | `r2` (if using Cloudflare R2, configure later) |
Also update the PostgreSQL password in `docker-compose.yml` (lines 9 and 40) to match.
## Step 11: Deploy with Docker Compose
```bash
cd ~/apps/orion
# Create directories with correct permissions for the container user
mkdir -p logs uploads exports
sudo chown -R 1000:1000 logs uploads exports
# Start infrastructure first
docker compose up -d db redis
# Wait for health checks to pass
docker compose ps
# Build and start the full stack
docker compose --profile full up -d --build
```
Verify all services are running:
```bash
docker compose --profile full ps
```
Expected: `api` (healthy), `db` (healthy), `redis` (healthy), `celery-worker` (healthy), `celery-beat` (running), `flower` (running).
## Step 12: Initialize Database
!!! note "PYTHONPATH required"
The seed scripts need `PYTHONPATH=/app` set explicitly when running inside the container.
```bash
# Run migrations (use 'heads' for multi-branch Alembic)
docker compose --profile full exec -e PYTHONPATH=/app api python -m alembic upgrade heads
# Seed production data
docker compose --profile full exec -e PYTHONPATH=/app api python scripts/seed/init_production.py
docker compose --profile full exec -e PYTHONPATH=/app api python scripts/seed/init_log_settings.py
docker compose --profile full exec -e PYTHONPATH=/app api python scripts/seed/create_default_content_pages.py
docker compose --profile full exec -e PYTHONPATH=/app api python scripts/seed/create_platform_pages.py
docker compose --profile full exec -e PYTHONPATH=/app api python scripts/seed/seed_email_templates.py
```
### Seeded Data Summary
| Data | Count |
|---|---|
| Admin users | 1 (`admin@wizamart.com`) |
| Platforms | 3 (OMS, Main, Loyalty+) |
| Admin settings | 15 |
| Subscription tiers | 4 (Essential, Professional, Business, Enterprise) |
| Log settings | 6 |
| CMS pages | 8 (About, Contact, FAQ, Shipping, Returns, Privacy, Terms, Homepage) |
| Email templates | 17 (4 languages: en, fr, de, lb) |
---
## Step 13: DNS Configuration (TODO)
Before setting up Caddy, point your domain's DNS to the server. In your domain registrar's DNS settings, create **A records**:
| Type | Name | Value | TTL |
|---|---|---|---|
| A | `@` | `91.99.65.229` | 300 |
| A | `api` | `91.99.65.229` | 300 |
| A | `git` | `91.99.65.229` | 300 |
| A | `flower` | `91.99.65.229` | 300 |
| AAAA | `@` | `2a01:4f8:1c1a:b39c::1` | 300 |
| AAAA | `api` | `2a01:4f8:1c1a:b39c::1` | 300 |
| AAAA | `git` | `2a01:4f8:1c1a:b39c::1` | 300 |
| AAAA | `flower` | `2a01:4f8:1c1a:b39c::1` | 300 |
!!! tip "DNS propagation"
Set TTL to 300 (5 minutes) initially. DNS changes can take up to 24 hours to propagate globally, but usually complete within 30 minutes. Verify with: `dig api.wizard.lu`
## Step 14: Reverse Proxy with Caddy (TODO)
Install Caddy:
```bash
sudo apt install -y debian-keyring debian-archive-keyring apt-transport-https
curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/gpg.key' \
| sudo gpg --dearmor -o /usr/share/keyrings/caddy-stable-archive-keyring.gpg
curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/debian.deb.txt' \
| sudo tee /etc/apt/sources.list.d/caddy-stable.list
sudo apt update && sudo apt install caddy
```
Configure `/etc/caddy/Caddyfile` (replace `wizard.lu` with your actual domain):
```caddy
api.wizard.lu {
reverse_proxy localhost:8001
}
git.wizard.lu {
reverse_proxy localhost:3000
}
flower.wizard.lu {
reverse_proxy localhost:5555
}
```
```bash
sudo systemctl restart caddy
```
Caddy automatically provisions Let's Encrypt SSL certificates.
After Caddy is working, remove the temporary firewall rules:
```bash
sudo ufw delete allow 3000/tcp
sudo ufw delete allow 8001/tcp
```
## Step 15: Gitea Actions Runner (TODO)
!!! warning "ARM64 architecture"
This server is ARM64. Download the `arm64` binary, not `amd64`.
```bash
mkdir -p ~/gitea-runner && cd ~/gitea-runner
# Download act_runner (ARM64 version)
wget https://gitea.com/gitea/act_runner/releases/latest/download/act_runner-linux-arm64
chmod +x act_runner-linux-arm64
# Register (get token from Gitea Site Administration > Runners)
./act_runner-linux-arm64 register \
--instance http://localhost:3000 \
--token YOUR_RUNNER_TOKEN
# Start daemon
./act_runner-linux-arm64 daemon &
```
## Step 16: Verify Full Deployment (TODO)
```bash
# All containers running
docker compose --profile full ps
# API health
curl http://localhost:8001/health
# Caddy proxy with SSL
curl https://api.wizard.lu/health
# Gitea
curl https://git.wizard.lu
```
---
## Port Reference
| Service | Internal | External | Domain (via Caddy) |
|---|---|---|---|
| Wizamart API | 8000 | 8001 | `api.wizard.lu` |
| PostgreSQL | 5432 | 5432 | (internal only) |
| Redis | 6379 | 6380 | (internal only) |
| Flower | 5555 | 5555 | `flower.wizard.lu` |
| Gitea | 3000 | 3000 | `git.wizard.lu` |
| Caddy | — | 80, 443 | (reverse proxy) |
## Directory Structure on Server
```
~/
├── gitea/
│ └── docker-compose.yml # Gitea + PostgreSQL
├── apps/
│ └── orion/ # Wizamart application
│ ├── .env # Production environment
│ ├── docker-compose.yml # App stack (API, DB, Redis, Celery)
│ ├── logs/ # Application logs
│ ├── uploads/ # User uploads
│ └── exports/ # Export files
└── gitea-runner/ # (TODO) CI/CD runner
```
## Troubleshooting
### Permission denied on logs
The Docker container runs as `appuser` (UID 1000). Host-mounted volumes need matching ownership:
```bash
sudo chown -R 1000:1000 logs uploads exports
```
### Celery workers restarting
Check logs for import errors:
```bash
docker compose --profile full logs celery-worker --tail 30
```
Common cause: stale task module references in `app/core/celery_config.py`.
### SSH service name on Ubuntu 24.04
Ubuntu 24.04 uses `ssh` not `sshd`:
```bash
sudo systemctl restart ssh # correct
sudo systemctl restart sshd # will fail
```
### git pull fails with local changes
If `docker-compose.yml` was edited on the server (e.g. passwords), stash before pulling:
```bash
git stash
git pull
git stash pop
```
## Maintenance
### View logs
```bash
docker compose --profile full logs -f api
docker compose --profile full logs -f celery-worker
```
### Restart services
```bash
docker compose --profile full restart api
```
### Update deployment
```bash
cd ~/apps/orion
git stash # Save local config changes
git pull
git stash pop # Re-apply local config
docker compose --profile full up -d --build
docker compose --profile full exec -e PYTHONPATH=/app api python -m alembic upgrade heads
```
### Quick access URLs (current — no domain yet)
| Service | URL |
|---|---|
| API Swagger docs | `http://91.99.65.229:8001/docs` |
| API ReDoc | `http://91.99.65.229:8001/redoc` |
| Admin panel | `http://91.99.65.229:8001/admin/login` |
| Health check | `http://91.99.65.229:8001/health` |
| Gitea | `http://91.99.65.229:3000` |
| Flower | `http://91.99.65.229:5555` |