File size: 7,480 Bytes
27c799c
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
# ============================================================================
# FASHIONISTAR — Production Docker Compose (Oracle Cloud Always Free VM)
# ============================================================================
# Deploys on: Oracle Cloud VM.Standard.A1.Flex (2 OCPUs / 12 GB RAM / ARM64)
#
# Services on Oracle (THIS FILE):
#   nginx      — Reverse proxy + SSL termination (Let's Encrypt)
#   api        — Django ASGI via Gunicorn + UvicornWorker  (separate API logs)
#   keepalive  — Oracle idle-prevention pinger (every 4 minutes)
#
# ❌ NO postgres container — uses Neon PostgreSQL (DATABASE_URL in .env.production)
# ❌ NO redis container   — uses Aiven Redis (REDIS_URL=rediss://... in .env.production)
#
# Services on Northflank (northflank.json):
#   celery_worker  — All background task queues  (separate Celery logs)
#   celery_beat    — Celery Beat scheduler (cron job)
#
# Both API (Oracle) and Celery (Northflank) share the SAME Aiven Redis
# for broker/cache — no cross-cloud Redis setup needed!
#
# Usage:
#   docker compose -f docker-compose.production.yml up -d
#   docker compose -f docker-compose.production.yml pull && \
#     docker compose -f docker-compose.production.yml up -d --no-deps api
# ============================================================================

version: '3.9'

services:

  # ── Nginx: Reverse proxy + SSL + Static files ───────────────────────────────
  nginx:
    image: nginx:1.27-alpine
    container_name: fashionistar_nginx
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./deploy/nginx/conf.d:/etc/nginx/conf.d:ro
      - /etc/letsencrypt:/etc/letsencrypt:ro
      - static_files:/app/staticfiles:ro
      - media_files:/app/mediafiles:ro
      - nginx_logs:/var/log/nginx
    depends_on:
      api:
        condition: service_healthy
    restart: always
    networks:
      - fashionistar_net
    logging:
      driver: "json-file"
      options:
        max-size: "50m"
        max-file: "5"
    healthcheck:
      test: ["CMD", "nginx", "-t"]
      interval: 30s
      timeout: 10s
      retries: 3

  # ── Django ASGI API Gateway ─────────────────────────────────────────────────
  # PORT=10000 (matches fashionistar_backend_production.env)
  # Uses Aiven Redis (TLS rediss://) from .env.production — NO local Redis needed
  # Uses Neon PostgreSQL (DATABASE_URL) from .env.production — NO local DB needed
  # Separate from Celery — has its own log volume (api_logs)
  api:
    image: ghcr.io/${GITHUB_REPOSITORY:-fashionistar-clothings/fashionistar_backend}/api:${IMAGE_TAG:-latest}
    container_name: fashionistar_api
    pull_policy: always
    # Load ALL production secrets from .env.production (mirrors fashionistar_backend_production.env)
    env_file:
      - .env.production
    environment:
      # ── Core Settings ───────────────────────────────────────────────────────
      - DJANGO_SETTINGS_MODULE=backend.config.production
      # PORT must be 10000 to match your fashionistar_backend_production.env
      - PORT=10000
      # ── Oracle Cloud VM: 2 OCPUs → 4 Gunicorn workers (2×CPU, I/O-bound) ──
      - GUNICORN_WORKERS=4
      - GUNICORN_TIMEOUT=300
      - GUNICORN_KEEPALIVE=75
      - GUNICORN_LOG_LEVEL=info
      # ── Updated Hosts — include Oracle domain ───────────────────────────────
      - ALLOWED_HOSTS=api.fashionistar.net,fashionistar.net,fashionistar-frontend.vercel.app,localhost,127.0.0.1
      - BACKEND_URL=https://api.fashionistar.net
      - BACKEND_TUNNEL_URL=https://api.fashionistar.net
      - SITE_URL=https://api.fashionistar.net
      - HEALTH_URL_ENDPOINT=https://api.fashionistar.net/api/v1/health
      # ── CORS / CSRF — include Oracle domain ────────────────────────────────
      - CORS_ALLOWED_ORIGINS=https://fashionistar.net,https://fashionistar-frontend.vercel.app,https://api.fashionistar.net
      - CSRF_TRUSTED_ORIGINS=https://fashionistar.net,https://fashionistar-frontend.vercel.app,https://api.fashionistar.net
      # ── Log directory — separate from Celery ───────────────────────────────
      - LOG_DIR=/app/logs/api
    volumes:
      - static_files:/app/staticfiles
      - media_files:/app/mediafiles
      # Dedicated API log volume — Celery logs are separate (on Northflank)
      - api_logs:/app/logs/api
    # No depends_on redis or postgres — both are external managed services
    restart: always
    deploy:
      resources:
        limits:
          # Oracle 2 OCPU / 12 GB RAM — reserve headroom for OS + logging
          cpus: '1.8'
          memory: 9G
        reservations:
          cpus: '0.5'
          memory: 2G
    networks:
      - fashionistar_net
    logging:
      driver: "json-file"
      options:
        max-size: "100m"
        max-file: "10"
        tag: "fashionistar-api"
    healthcheck:
      # PORT=10000 (matches production env)
      test: ["CMD", "curl", "-fsS", "http://127.0.0.1:10000/api/v1/health/"]
      interval: 30s
      timeout: 10s
      start_period: 90s
      retries: 5

  # ── Oracle Keep-Alive (Idle Prevention) ─────────────────────────────────────
  # Oracle reclaims Always Free instances if CPU < 10% for 7 consecutive days.
  # This lightweight pinger on PORT=10000 prevents idle reclamation.
  keepalive:
    image: curlimages/curl:8-alpine
    container_name: fashionistar_keepalive
    command: >
      sh -c "while true; do
        curl -fsS http://api:10000/api/v1/health/ > /dev/null 2>&1 || true;
        sleep 240;
      done"
    depends_on:
      - api
    restart: always
    deploy:
      resources:
        limits:
          cpus: '0.05'
          memory: 32M
    networks:
      - fashionistar_net
    logging:
      driver: "json-file"
      options:
        max-size: "5m"
        max-file: "2"

# ── Volumes ──────────────────────────────────────────────────────────────────
volumes:
  static_files:
    name: fashionistar_static_files
  media_files:
    name: fashionistar_media_files
  # Dedicated API log volume (Django ASGI gateway logs only — Celery logs on Northflank)
  api_logs:
    name: fashionistar_api_logs
    driver: local
    driver_opts:
      type: none
      o: bind
      device: /var/log/fashionistar/api
  # Nginx access/error logs
  nginx_logs:
    name: fashionistar_nginx_logs
    driver: local
    driver_opts:
      type: none
      o: bind
      device: /var/log/fashionistar/nginx

# ── Networks ─────────────────────────────────────────────────────────────────
networks:
  fashionistar_net:
    name: fashionistar_production_net
    driver: bridge
    ipam:
      driver: default
      config:
        - subnet: 172.20.0.0/16