Skip to content
Merged

Dev: #377

Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
120 changes: 66 additions & 54 deletions .github/workflows/cd.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,9 @@ on:

env:
REGISTRY: ghcr.io
# e.g. ghcr.io/devxtra-community/hayon
IMAGE_BASE: ghcr.io/${{ github.repository_owner }}/hayon
# Opt into Node.js 24 to suppress deprecation warnings from actions
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true

jobs:
# ============================================================
Expand Down Expand Up @@ -83,6 +84,8 @@ jobs:
NEXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID=${{ secrets.NEXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID }}
NEXT_PUBLIC_FIREBASE_APP_ID=${{ secrets.NEXT_PUBLIC_FIREBASE_APP_ID }}
NEXT_PUBLIC_FIREBASE_MEASUREMENT_ID=${{ secrets.NEXT_PUBLIC_FIREBASE_MEASUREMENT_ID }}
NEXT_PUBLIC_POSTHOG_KEY=${{ secrets.NEXT_PUBLIC_POSTHOG_KEY }}
NEXT_PUBLIC_POSTHOG_HOST=${{ secrets.NEXT_PUBLIC_POSTHOG_HOST }}

# ---- Build & push custom RabbitMQ image ----
- name: Build and push rabbitmq
Expand Down Expand Up @@ -114,74 +117,83 @@ jobs:
chmod 600 ~/.ssh/ec2.pem
ssh-keyscan -H ${{ secrets.EC2_HOST }} >> ~/.ssh/known_hosts

# ---- Sync config files (compose, nginx, rabbitmq config) ----
# We only need non-built files. Source code is NOT needed on EC2.
# ---- Sync config files to EC2 ----
# --relative preserves full directory structure:
# nginx/nginx.conf → $APP_DIR/nginx/nginx.conf ✅
# rabbitmq/enabled_plugins → $APP_DIR/rabbitmq/enabled_plugins ✅
# Without --relative, a trailing slash on nginx/ strips the folder name
# and dumps nginx.conf directly into $APP_DIR/ — that's what caused the error.
- name: Sync config files to EC2
run: |
rsync -avz \
rsync -avz --relative \
-e "ssh -i ~/.ssh/ec2.pem" \
docker-compose.prod.yml \
nginx/ \
rabbitmq/enabled_plugins \
scripts/ \
./docker-compose.prod.yml \
./nginx/nginx.conf \
./rabbitmq/enabled_plugins \
./scripts/ \
${{ secrets.EC2_USER }}@${{ secrets.EC2_HOST }}:${{ secrets.APP_DIR }}/

# ---- Write backend .env ----
# Using printf per line instead of heredoc to avoid YAML-indentation
# leaking as leading spaces into the .env file.
- name: Write backend .env on EC2
run: |
ssh -i ~/.ssh/ec2.pem ${{ secrets.EC2_USER }}@${{ secrets.EC2_HOST }} bash << 'ENDSSH'
mkdir -p ${{ secrets.APP_DIR }}/backend
cat > ${{ secrets.APP_DIR }}/backend/.env << 'EOF'
NODE_ENV=production
PORT=5000
FRONTEND_URL=${{ secrets.FRONTEND_URL }}
BACKEND_URL=${{ secrets.BACKEND_URL }}
GOOGLE_CLIENT_ID=${{ secrets.GOOGLE_CLIENT_ID }}
GOOGLE_CLIENT_SECRET=${{ secrets.GOOGLE_CLIENT_SECRET }}
GOOGLE_CALLBACK_URL=${{ secrets.GOOGLE_CALLBACK_URL }}
MONGODB_URI=${{ secrets.MONGODB_URI }}
ACCESS_TOKEN_SECRET=${{ secrets.ACCESS_TOKEN_SECRET }}
REFRESH_TOKEN_SECRET=${{ secrets.REFRESH_TOKEN_SECRET }}
JWT_EXPIRES_IN=7d
STRIPE_SECRET_KEY=${{ secrets.STRIPE_SECRET_KEY }}
STRIPE_PUBLISHABLE_KEY=${{ secrets.STRIPE_PUBLISHABLE_KEY }}
STRIPE_PRO_PRICE_ID=${{ secrets.STRIPE_PRO_PRICE_ID }}
STRIPE_WEBHOOK_SECRET=${{ secrets.STRIPE_WEBHOOK_SECRET }}
EMAIL_USER=${{ secrets.EMAIL_USER }}
EMAIL_PASS=${{ secrets.EMAIL_PASS }}
AWS_ACCESS_KEY_ID=${{ secrets.AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY=${{ secrets.AWS_SECRET_ACCESS_KEY }}
AWS_REGION=${{ secrets.AWS_REGION }}
AWS_S3_BUCKET_NAME=${{ secrets.AWS_S3_BUCKET_NAME }}
TUMBLR_CONSUMER_KEY=${{ secrets.TUMBLR_CONSUMER_KEY }}
TUMBLR_CONSUMER_SECRET=${{ secrets.TUMBLR_CONSUMER_SECRET }}
META_APP_ID=${{ secrets.META_APP_ID }}
META_APP_SECRET=${{ secrets.META_APP_SECRET }}
META_REDIRECT_URI=${{ secrets.META_REDIRECT_URI }}
THREADS_APP_ID=${{ secrets.THREADS_APP_ID }}
THREADS_APP_SECRET=${{ secrets.THREADS_APP_SECRET }}
THREADS_REDIRECT_URI=${{ secrets.THREADS_REDIRECT_URI }}
MASTODON_CALLBACK_URL=${{ secrets.MASTODON_CALLBACK_URL }}
MASTODON_CLIENT_KEY=${{ secrets.MASTODON_CLIENT_KEY }}
MASTODON_CLIENT_SECRET=${{ secrets.MASTODON_CLIENT_SECRET }}
MASTODON_INSTANCE_URL=https://mastodon.social
RABBITMQ_URL=amqp://${{ secrets.RABBITMQ_USER }}:${{ secrets.RABBITMQ_PASS }}@rabbitmq:5672
REDIS_HOST=redis
REDIS_PORT=6379
GEMINI_API_KEY=${{ secrets.GEMINI_API_KEY }}
BETTER_STACK_TOKEN=${{ secrets.BETTER_STACK_TOKEN }}
EOF
ENV_FILE="${{ secrets.APP_DIR }}/backend/.env"
printf '%s\n' \
'NODE_ENV=production' \
'PORT=5000' \
'FRONTEND_URL=${{ secrets.FRONTEND_URL }}' \
'BACKEND_URL=${{ secrets.BACKEND_URL }}' \
'GOOGLE_CLIENT_ID=${{ secrets.GOOGLE_CLIENT_ID }}' \
'GOOGLE_CLIENT_SECRET=${{ secrets.GOOGLE_CLIENT_SECRET }}' \
'GOOGLE_CALLBACK_URL=${{ secrets.GOOGLE_CALLBACK_URL }}' \
'MONGODB_URI=${{ secrets.MONGODB_URI }}' \
'ACCESS_TOKEN_SECRET=${{ secrets.ACCESS_TOKEN_SECRET }}' \
'REFRESH_TOKEN_SECRET=${{ secrets.REFRESH_TOKEN_SECRET }}' \
'JWT_EXPIRES_IN=7d' \
'STRIPE_SECRET_KEY=${{ secrets.STRIPE_SECRET_KEY }}' \
'STRIPE_PUBLISHABLE_KEY=${{ secrets.STRIPE_PUBLISHABLE_KEY }}' \
'STRIPE_PRO_PRICE_ID=${{ secrets.STRIPE_PRO_PRICE_ID }}' \
'STRIPE_WEBHOOK_SECRET=${{ secrets.STRIPE_WEBHOOK_SECRET }}' \
'EMAIL_USER=${{ secrets.EMAIL_USER }}' \
'EMAIL_PASS=${{ secrets.EMAIL_PASS }}' \
'AWS_ACCESS_KEY_ID=${{ secrets.AWS_ACCESS_KEY_ID }}' \
'AWS_SECRET_ACCESS_KEY=${{ secrets.AWS_SECRET_ACCESS_KEY }}' \
'AWS_REGION=${{ secrets.AWS_REGION }}' \
'AWS_S3_BUCKET_NAME=${{ secrets.AWS_S3_BUCKET_NAME }}' \
'TUMBLR_CONSUMER_KEY=${{ secrets.TUMBLR_CONSUMER_KEY }}' \
'TUMBLR_CONSUMER_SECRET=${{ secrets.TUMBLR_CONSUMER_SECRET }}' \
'META_APP_ID=${{ secrets.META_APP_ID }}' \
'META_APP_SECRET=${{ secrets.META_APP_SECRET }}' \
'META_REDIRECT_URI=${{ secrets.META_REDIRECT_URI }}' \
'THREADS_APP_ID=${{ secrets.THREADS_APP_ID }}' \
'THREADS_APP_SECRET=${{ secrets.THREADS_APP_SECRET }}' \
'THREADS_REDIRECT_URI=${{ secrets.THREADS_REDIRECT_URI }}' \
'MASTODON_CALLBACK_URL=${{ secrets.MASTODON_CALLBACK_URL }}' \
'MASTODON_CLIENT_KEY=${{ secrets.MASTODON_CLIENT_KEY }}' \
'MASTODON_CLIENT_SECRET=${{ secrets.MASTODON_CLIENT_SECRET }}' \
'MASTODON_INSTANCE_URL=https://mastodon.social' \
'RABBITMQ_URL=amqp://${{ secrets.RABBITMQ_USER }}:${{ secrets.RABBITMQ_PASS }}@rabbitmq:5672' \
'REDIS_HOST=redis' \
'REDIS_PORT=6379' \
'GEMINI_API_KEY=${{ secrets.GEMINI_API_KEY }}' \
'BETTER_STACK_TOKEN=${{ secrets.BETTER_STACK_TOKEN }}' \
> "$ENV_FILE"
echo "backend .env written:"
cat "$ENV_FILE"
ENDSSH

# ---- Write root .env (registry + rabbitmq creds for compose) ----
# ---- Write root .env (IMAGE_BASE + rabbitmq creds for compose) ----
- name: Write root .env on EC2
run: |
ssh -i ~/.ssh/ec2.pem ${{ secrets.EC2_USER }}@${{ secrets.EC2_HOST }} bash << 'ENDSSH'
cat > ${{ secrets.APP_DIR }}/.env << 'EOF'
RABBITMQ_USER=${{ secrets.RABBITMQ_USER }}
RABBITMQ_PASS=${{ secrets.RABBITMQ_PASS }}
IMAGE_BASE=ghcr.io/${{ github.repository_owner }}/hayon
EOF
printf '%s\n' \
'RABBITMQ_USER=${{ secrets.RABBITMQ_USER }}' \
'RABBITMQ_PASS=${{ secrets.RABBITMQ_PASS }}' \
'IMAGE_BASE=ghcr.io/${{ github.repository_owner }}/hayon' \
> "${{ secrets.APP_DIR }}/.env"
ENDSSH

# ---- Pull pre-built images and restart ----
Expand Down
2 changes: 2 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,8 @@ services:
NEXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID: ${NEXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID}
NEXT_PUBLIC_FIREBASE_APP_ID: ${NEXT_PUBLIC_FIREBASE_APP_ID}
NEXT_PUBLIC_FIREBASE_MEASUREMENT_ID: ${NEXT_PUBLIC_FIREBASE_MEASUREMENT_ID}
NEXT_PUBLIC_POSTHOG_KEY: ${NEXT_PUBLIC_POSTHOG_KEY}
NEXT_PUBLIC_POSTHOG_HOST: ${NEXT_PUBLIC_POSTHOG_HOST}
container_name: hayon_frontend
expose:
- "3000"
Expand Down
4 changes: 4 additions & 0 deletions frontend/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@ ARG NEXT_PUBLIC_FIREBASE_STORAGE_BUCKET
ARG NEXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID
ARG NEXT_PUBLIC_FIREBASE_APP_ID
ARG NEXT_PUBLIC_FIREBASE_MEASUREMENT_ID
ARG NEXT_PUBLIC_POSTHOG_KEY
ARG NEXT_PUBLIC_POSTHOG_HOST

ENV NEXT_PUBLIC_API_URL=$NEXT_PUBLIC_API_URL
ENV NEXT_PUBLIC_VAPID_KEY=$NEXT_PUBLIC_VAPID_KEY
Expand All @@ -42,6 +44,8 @@ ENV NEXT_PUBLIC_FIREBASE_STORAGE_BUCKET=$NEXT_PUBLIC_FIREBASE_STORAGE_BUCKET
ENV NEXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID=$NEXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID
ENV NEXT_PUBLIC_FIREBASE_APP_ID=$NEXT_PUBLIC_FIREBASE_APP_ID
ENV NEXT_PUBLIC_FIREBASE_MEASUREMENT_ID=$NEXT_PUBLIC_FIREBASE_MEASUREMENT_ID
ENV NEXT_PUBLIC_POSTHOG_KEY=$NEXT_PUBLIC_POSTHOG_KEY
ENV NEXT_PUBLIC_POSTHOG_HOST=$NEXT_PUBLIC_POSTHOG_HOST


RUN sed -i "s|NEXT_PUBLIC_FIREBASE_API_KEY_PLACEHOLDER|${NEXT_PUBLIC_FIREBASE_API_KEY}|g" /app/frontend/public/firebase-messaging-sw.js && \
Expand Down
9 changes: 9 additions & 0 deletions frontend/next.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,15 @@ const nextConfig: NextConfig = {
protocol: "https",
hostname: "hayon-app-images.s3.ap-south-1.amazonaws.com",
},
// hayon-app-images-2 is the actual production bucket
{
protocol: "https",
hostname: "hayon-app-images-2.s3.amazonaws.com",
},
{
protocol: "https",
hostname: "hayon-app-images-2.s3.ap-south-1.amazonaws.com",
},
{
protocol: "https",
hostname: "github.com",
Expand Down
31 changes: 17 additions & 14 deletions nginx/nginx.conf
Original file line number Diff line number Diff line change
Expand Up @@ -17,16 +17,13 @@ http {
gzip on;
gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript;

# --------------------------------------------------------
# Upstream definitions
# --------------------------------------------------------
upstream frontend {
server frontend:3000;
}

upstream backend {
server backend:5000;
}
# Docker's internal DNS resolver.
# Forces nginx to re-resolve upstream hostnames dynamically on each request.
# Critical: when backend/frontend containers are recreated (new deploy),
# they get new internal IPs. Without this resolver + variable proxy_pass,
# nginx caches the old IP at startup and gets ECONNREFUSED → 502.
resolver 127.0.0.11 valid=10s ipv6=off;
resolver_timeout 5s;

# --------------------------------------------------------
# HTTP → HTTPS redirect (handles ALL domains)
Expand Down Expand Up @@ -82,7 +79,10 @@ http {
client_max_body_size 50m;

location / {
proxy_pass http://frontend;
# Variable-based proxy_pass forces nginx to use the resolver
# for DNS lookup on every request, not just at startup.
set $frontend_upstream http://frontend:3000;
proxy_pass $frontend_upstream;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade';
Expand Down Expand Up @@ -117,7 +117,8 @@ http {

# All API routes
location /api/ {
proxy_pass http://backend;
set $backend_upstream http://backend:5000;
proxy_pass $backend_upstream;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade';
Expand All @@ -131,7 +132,8 @@ http {

# WebSocket (Socket.io)
location /socket.io/ {
proxy_pass http://backend;
set $backend_upstream http://backend:5000;
proxy_pass $backend_upstream;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
Expand All @@ -144,7 +146,8 @@ http {

# Health check
location /health {
proxy_pass http://backend;
set $backend_upstream http://backend:5000;
proxy_pass $backend_upstream;
proxy_set_header Host $host;
}
}
Expand Down
Loading