diff --git a/.gitea/workflows/ci.yml b/.gitea/workflows/ci.yml index decde0b..879e145 100644 --- a/.gitea/workflows/ci.yml +++ b/.gitea/workflows/ci.yml @@ -2,6 +2,9 @@ name: CI on: pull_request: + push: + branches: + - main schedule: - cron: "0 2 * * *" @@ -188,3 +191,15 @@ jobs: - name: Remove CI image if: always() run: docker image rm -f "$CI_IMAGE" || true + + deploy: + if: github.event_name == 'push' && github.ref == 'refs/heads/main' + runs-on: ubuntu-latest + steps: + - name: Deploy to lintel-prod-01 + uses: appleboy/ssh-action@v1 + with: + host: ${{ secrets.PROD_SSH_HOST }} + username: deploy + key: ${{ secrets.PROD_SSH_KEY }} + script: bash /srv/sum/nohype/app/deploy/deploy.sh diff --git a/config/settings/production.py b/config/settings/production.py index abd04c6..08c3c95 100644 --- a/config/settings/production.py +++ b/config/settings/production.py @@ -2,8 +2,16 @@ from .base import * # noqa DEBUG = False +# Behind Caddy: trust the forwarded proto header so Django knows it's HTTPS. +# SECURE_SSL_REDIRECT is intentionally off — Caddy handles HTTPS redirects +# before the request reaches Django; enabling it here causes redirect loops. SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https") USE_X_FORWARDED_HOST = True -SECURE_SSL_REDIRECT = True +SECURE_SSL_REDIRECT = False SESSION_COOKIE_SECURE = True CSRF_COOKIE_SECURE = True + +CSRF_TRUSTED_ORIGINS = [ + "https://nohypeai.net", + "https://www.nohypeai.net", +] diff --git a/deploy/caddy/nohype.caddy b/deploy/caddy/nohype.caddy new file mode 100644 index 0000000..4faa1f6 --- /dev/null +++ b/deploy/caddy/nohype.caddy @@ -0,0 +1,23 @@ +nohypeai.net, www.nohypeai.net { + encode gzip zstd + + header { + X-Content-Type-Options nosniff + X-Frame-Options DENY + Referrer-Policy strict-origin-when-cross-origin + Permissions-Policy "geolocation=(), microphone=(), camera=()" + X-Forwarded-Proto https + } + + handle_path /static/* { + root * /srv/sum/nohype/static + file_server + } + + handle_path /media/* { + root * /srv/sum/nohype/media + file_server + } + + reverse_proxy localhost:8001 +} diff --git a/deploy/deploy.sh b/deploy/deploy.sh new file mode 100755 index 0000000..470365a --- /dev/null +++ b/deploy/deploy.sh @@ -0,0 +1,34 @@ +#!/usr/bin/env bash +# Deploy script for No Hype AI — runs on lintel-prod-01 as deploy user. +# Called by CI after a successful push to main. +set -euo pipefail + +SITE_DIR=/srv/sum/nohype +APP_DIR=${SITE_DIR}/app + +echo "==> Pulling latest code" +git -C "${APP_DIR}" pull origin main + +echo "==> Updating compose file" +cp "${APP_DIR}/docker-compose.prod.yml" "${SITE_DIR}/docker-compose.prod.yml" + +echo "==> Ensuring static/media directories exist" +mkdir -p "${SITE_DIR}/static" "${SITE_DIR}/media" + +echo "==> Building image" +docker compose -f "${SITE_DIR}/docker-compose.prod.yml" build --no-cache + +echo "==> Restarting service" +sudo systemctl restart sum-nohype + +echo "==> Waiting for health check" +for i in $(seq 1 30); do + if curl -fsS http://localhost:8001/ >/dev/null 2>&1; then + echo "==> Site is up" + exit 0 + fi + sleep 3 +done +echo "ERROR: site did not come up after 90s" >&2 +sudo journalctl -u sum-nohype --no-pager -n 50 +exit 1 diff --git a/deploy/sum-nohype.service b/deploy/sum-nohype.service new file mode 100644 index 0000000..81aef8d --- /dev/null +++ b/deploy/sum-nohype.service @@ -0,0 +1,26 @@ +[Unit] +Description=No Hype AI (Docker Compose) +Requires=docker.service +After=docker.service network-online.target + +[Service] +Type=simple +User=deploy +Group=www-data +WorkingDirectory=/srv/sum/nohype + +ExecStartPre=docker compose -f docker-compose.prod.yml pull --ignore-pull-failures +ExecStart=docker compose -f docker-compose.prod.yml up --build +ExecStop=docker compose -f docker-compose.prod.yml down + +Restart=on-failure +RestartSec=10 +TimeoutStartSec=300 +TimeoutStopSec=30 + +StandardOutput=journal +StandardError=journal +SyslogIdentifier=sum-nohype + +[Install] +WantedBy=multi-user.target diff --git a/docker-compose.prod.yml b/docker-compose.prod.yml new file mode 100644 index 0000000..e2c63ad --- /dev/null +++ b/docker-compose.prod.yml @@ -0,0 +1,46 @@ +services: + web: + build: . + working_dir: /app + command: > + sh -c "python manage.py tailwind install --no-input && + python manage.py tailwind build && + python manage.py migrate --noinput && + python manage.py collectstatic --noinput && + gunicorn config.wsgi:application + --workers 3 + --bind 0.0.0.0:8000 + --access-logfile - + --error-logfile - + --capture-output" + env_file: .env + environment: + DJANGO_SETTINGS_MODULE: config.settings.production + volumes: + - /srv/sum/nohype/static:/app/staticfiles + - /srv/sum/nohype/media:/app/media + ports: + - "127.0.0.1:8001:8000" + depends_on: + db: + condition: service_healthy + restart: unless-stopped + + db: + image: postgres:16-alpine + env_file: .env + environment: + POSTGRES_DB: nohype + POSTGRES_USER: nohype + volumes: + - nohype_pg:/var/lib/postgresql/data + healthcheck: + test: ["CMD-SHELL", "pg_isready -U nohype -d nohype"] + interval: 5s + timeout: 5s + retries: 10 + start_period: 10s + restart: unless-stopped + +volumes: + nohype_pg: diff --git a/docker-compose.yml b/docker-compose.yml index b73c903..50e3f11 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -3,7 +3,9 @@ services: build: . working_dir: /app command: > - sh -c "python manage.py migrate --noinput && + sh -c "python manage.py tailwind install --no-input && + python manage.py tailwind build && + python manage.py migrate --noinput && python manage.py seed_e2e_content && python manage.py runserver 0.0.0.0:8000" volumes: