From d9060ed6eace48987a0ab7b235d226d05e9d2fe9 Mon Sep 17 00:00:00 2001 From: Samir Boulahtit Date: Wed, 11 Feb 2026 23:23:24 +0100 Subject: [PATCH] docs(deployment): add Hetzner Cloud server setup guide Complete step-by-step guide documenting the server setup performed on 2026-02-11: - Server hardening (non-root user, UFW, SSH lockdown, fail2ban) - Docker & Docker Compose installation - Gitea self-hosted git with PostgreSQL - Wizamart deployment (API, DB, Redis, Celery, Flower) - Database migration and production seeding - Troubleshooting section for issues encountered during setup - DNS and Caddy reverse proxy instructions (TODO for next session) Co-Authored-By: Claude Opus 4.6 --- docs/deployment/hetzner-server-setup.md | 540 ++++++++++++++++++++++++ mkdocs.yml | 1 + 2 files changed, 541 insertions(+) create mode 100644 docs/deployment/hetzner-server-setup.md diff --git a/docs/deployment/hetzner-server-setup.md b/docs/deployment/hetzner-server-setup.md new file mode 100644 index 00000000..289cba23 --- /dev/null +++ b/docs/deployment/hetzner-server-setup.md @@ -0,0 +1,540 @@ +# 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 1–12):** + + - 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 13–15):** + + - [ ] 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= + - 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: + 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 `` 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.yourdomain.com` + +## 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 `yourdomain.com` with your actual domain): + +```caddy +api.yourdomain.com { + reverse_proxy localhost:8001 +} + +git.yourdomain.com { + reverse_proxy localhost:3000 +} + +flower.yourdomain.com { + 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.yourdomain.com/health + +# Gitea +curl https://git.yourdomain.com +``` + +--- + +## Port Reference + +| Service | Internal | External | Domain (via Caddy) | +|---|---|---|---| +| Wizamart API | 8000 | 8001 | `api.yourdomain.com` | +| PostgreSQL | 5432 | 5432 | (internal only) | +| Redis | 6379 | 6380 | (internal only) | +| Flower | 5555 | 5555 | `flower.yourdomain.com` | +| Gitea | 3000 | 3000 | `git.yourdomain.com` | +| 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` | diff --git a/mkdocs.yml b/mkdocs.yml index cff2a72d..2a1adadd 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -197,6 +197,7 @@ nav: - CloudFlare Setup: deployment/cloudflare.md - GitLab CI/CD: deployment/gitlab.md - Gitea CI/CD: deployment/gitea.md + - Hetzner Server Setup: deployment/hetzner-server-setup.md - Environment Variables: deployment/environment.md - Stripe Integration: deployment/stripe-integration.md