Merge pull request 'feat: production deploy pipeline' (#14) from feat/deploy-pipeline into main
Some checks failed
CI / ci (push) Has been skipped
CI / pr-e2e (push) Has been skipped
CI / nightly-e2e (push) Has been skipped
CI / deploy (push) Failing after 5s

Reviewed-on: #14
This commit was merged in pull request #14.
This commit is contained in:
2026-02-28 21:40:03 +00:00
7 changed files with 156 additions and 2 deletions

View File

@@ -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

View File

@@ -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",
]

23
deploy/caddy/nohype.caddy Normal file
View File

@@ -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
}

34
deploy/deploy.sh Executable file
View File

@@ -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

26
deploy/sum-nohype.service Normal file
View File

@@ -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

46
docker-compose.prod.yml Normal file
View File

@@ -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:

View File

@@ -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: