اگر برنامه django خود را نوشته اید و حالا می خواهید آن را به آسان ترین، بهینه ترین و تمیزترین شکل، روی سرور مجازی deploy کنید، این مقاله برای شماست!
پیش از هر چیزی باید داکر را روی vps نصب کرده باشید که این، لینک آموزش نصب داکر است. در قدم بعدی اگر سرور ایران دارید باید به طریق تحریم های مروبط به docker hub را رفع کنید. ما تا کنون به 2 روش برای این کار پرداخته ایم:
تغییر dns های سرور برای استفاده از docker در شرایط تحریم
ساخت docker rgistry شخصی برای استفاده از داکر در ایران
پس از این می توانیم سراغ کار اصلی برویم. در ابتدا لازم به ذکر است ساختار استاندارد دایرکتوری ها یا با اصطلاح best practice برای deployment در اکثر پروژهای حرفه ای که دیده ام به این شکل است:
├── .env.prod ├── .env.prod.db ├── .gitignore ├── core │ ├── src │ │ ├── __init__.py │ │ ├── asgi.py │ │ ├── settings.py │ │ ├── urls.py │ │ └── wsgi.py │ ├── manage.py ├── Dockerfile ├── docker-compose.yml ├── README.md ├── requirement.txt └── nginx ├── Dockerfile └── nginx.conf
طبق این ساختار کد های ما و ماژول های برنامه جنگویی ما درون دایرکتوری ای برای مثال به نام core قرار گرفته و فایل های مربوط به deployment و مسائل devops از کدها جدا شده و در دایرکتوری والد قرار می گیرند. گمان می کنم دلیل این کار این باشد که اشاره به دایرکتوری های فرزند از درون دایرکتوری والد کاری راحت است اما اشاره به دایرکتوری های پیشین از درون دایرکتوری های فرزند کاری پر دردسر است و طبق تجربه شخصی در فایل های docker-compose با خطا مواجه می شویم. علاوه بر این که جدا کردن فایل های development و devops از نظر منطق repository کار درستی است.
بهر حال اگر ساختار دیگری جز این را مدنظر دارید با کمی تغییر در آدرس دهی های نسبی، می توانید به آن برسید. در این آموزش اما همین ساختار را در نظر می گیریم.
ما به 2 داکرفایل نیاز داریم. یکی برای nginx و دیگری برای python و نصب وابستگی های پایتونی که در فایل requirements.txt هستند. ابتدا به Dockerfile مربوط به سرویس nginx می پردازیم:
FROM nginx:1.23-alpine RUN rm /etc/nginx/conf.d/default.conf COPY nginx.conf /etc/nginx/conf.d
در ابتدا نسخه آلپاین image مربوط به nginx را بعنوان image پایه این Dockerfile معرفی کردیم و سپس کانفیگ پیش فرض را پاک کرده و کانفیگ خودمان به نام nginx.conf را درون دایرکتوری conf.d کپی کردیم. لازم به ذکر است که هر فایل .conf که درون این دایرکتوری باشد، به طور خودکار توسط nginx بعنوان فایل کانفیگ در نظر گرفته خواهد شد.
حالا ببینیم محتویات این فایل کانفیک چه باید باشد. در باره فایل nginx.conf بعدتر توضیح خواهیم داد.
حالا نوبت به Dockerfile پایتون می رسد. پیش از توضیح این داکرفایل لازم به ذکر است که برای کاهش حجم نهایی image ساخته شده از روش multi stage build استفاده کرده ایم. اگر تجربه front-end داشته باشید یا با npm آشنا باشید می توانید برای درک بهتر این روش آن را همانند موقعی فرض کنید که دستور npm run production را می زنیم و فایل ها و وابستگی های مورد نیاز ما ساخته شده و دیگر نیازی به پوشه حجیم node module نخواهیم داشت. فلذا از دیدگاه داکری می توانیم بگوییم در این روش کاری می کنیم که فقط binary file های مورد نیازمان را در ایمیج django که خواهیم ساخت، داشته باشیم. در ادامه بیشتر توضیح خواهیم داد. فایل Dockerfile موجود در root پروژه:
########### # BUILDER # ########### # pull official base image FROM python:3.10-alpine as builder # set work directory WORKDIR /usr/src/app # set environment variables ENV PYTHONDONTWRITEBYTECODE 1 ENV PYTHONUNBUFFERED 1 # install psycopg2 dependencies RUN apk update \ && apk add postgresql-dev gcc python3-dev musl-dev nano # lint RUN pip install --upgrade pip #RUN pip install flake8==3.9.2 COPY ./core . #RUN flake8 --ignore=E501,F401 . # install dependencies COPY ./requirements.txt . RUN pip wheel --no-cache-dir --no-deps --wheel-dir /usr/src/app/wheels -r requirements.txt ######### # FINAL # ######### # pull official base image FROM python:3.10-alpine # create directory for the app user RUN mkdir -p /home/app # create the app user RUN addgroup -S --gid 1000 app && adduser -S app --uid 1000 -G app # create the appropriate directories ENV HOME=/home/app ENV APP_HOME=/home/app/web RUN mkdir $APP_HOME WORKDIR $APP_HOME # install dependencies RUN apk update && apk add libpq COPY --from=builder /usr/src/app/wheels /wheels COPY --from=builder /usr/src/app/requirements.txt . RUN pip install --no-cache /wheels/* # copy project COPY ./core $APP_HOME # chown all the files to the app user RUN chown -R app:app $APP_HOME #RUN python ./manage.py collectstatic # copy entrypoint.sh COPY core/entrypoint.sh /home/app/web RUN sed -i 's/\r$//g' $APP_HOME/entrypoint.sh & chmod o+x $APP_HOME/entrypoint.sh # change to the app user USER app # run entrypoint.sh #ENTRYPOINT ["/home/app/web/entrypoint.sh"]
پیش از هر توضیحی لازم است بگوییم برخی قسمت ها برای حفظ سادگی آموزش کامنت شده اند و اگر به این موارد نیاز داشتید کافی است آن ها را uncomment کنید.
خب، در فایل بالا پیش از هر دستور کامنتی گذاشته شده و بسیاری از موارد نیز مربوط به دستورات سیستم عامل لینکوکس می باشند که شاید توضیح آنها چندان ربطی به این مقاله نداشته باشد. همچنین می توانید با کمک chat gpt اطلاعات بیشتری از این دستورات و آپشن ها به دست بیاورید. ولی بعنوان نکته ای مهم که ممکن است برای برخی مشکل ساز بشود، دقت کنید که چون به دلایل امنیتی نمی خواهیم کاربر root را به کانتینر اختصاص دهیم، فلذا ابتدا با دستور زیر یک گروه دسترسی و یک کاربر با id های 1000 ساختیم و آن گروه را به کاربر اختصاص دادیم. سپس در ادامه کاربر و گروه تمام فایل های پروژه را به کاربر و گروه app که ساخته ایم، تغییر دادیم:
RUN addgroup -S --gid 1000 app && adduser -S app --uid 1000 -G app
...
RUN chown -R app:app $APP_HOME
اما در صورتی که با مسئولیت خودتان، میخواستید امکان فعالیت کاربر بعنوان root درون کانتینر وجود داشته باشد می توانستید به جای موارد بالا از این دستور استفاده کنید:
USER root
همچنین فیل entrypoint.sh که اجرای آن را کامنت کرده ایم، مشخص می کند که اجرای کانتینر با چه فرآیندی شروع شود. برای مثال اگر می خواستیم این کانتینر همراه با تست سالم بودن کانکشن postgres مان شروع شود، ابتدا ENTRYPOINT ["/home/app/web/entrypoint.sh"] را از کامنت در می آوردیم و سپس برای مثال فایلی به نام entrypoint.sh با محتویات زیر می ساختیم:
#!/usr/bin/env sh if [ "$DATABASE" = "postgres" ] then echo "Waiting for postgres..." while ! nc -z $SQL_HOST $SQL_PORT; do sleep 0.1 done echo "PostgreSQL started" fi exec "$@"
همانطور که می دانید برای ساخته شدن و بالا آوردن چند کانتینر در داکر باید از docker compose استفاده کنیم. برای این کار یک فایل به نام docker-compose.yml در پوشه اصلی پروژه ایجاد می کنیم و محتویات زیر را درون آن می نویسیم:
version: "3.9" services: backend: build: . container_name: backend command: gunicorn src.wsgi:application --bind 0.0.0.0:8000 restart: always volumes: - ./core/:/home/app/web/ - ./core/static:/home/app/web/static - ./core/media:/home/app/web/media expose: - "8000" env_file: - ./.env.prod depends_on: - db - redis db: image: postgres:15-alpine restart: always volumes: - postgres_data:/var/lib/postgresql/data/ env_file: - ./.env.prod.db redis: container_name: redis image: redis restart: always ports: - "6379:6379" command: redis-server --appendonly yes --replica-read-only no --save 60 1 --loglevel warning nginx: build: ./nginx restart: always ports: - "80:80" volumes: - ./core/static:/home/app/web/static - ./core/media:/home/app/web/media depends_on: - redis - backend
در فایل بالا ما چند کانتینر ایجاد کردیم. backend که همان پروژه جنگوی ماست و براساس Dockerfile که پیشتر توضیح دادیم روی پورت 8000 به اصطلاح expose می شود. یعنی پورت 8000 هاست یا سرور ما به پورت 8000 کانتینتر تعلق میگیرد. gunicorn یک سرور اپلیکیشن wsgi است که بعنوان واسط بین اپلیکیشن جنگوی ما و nginx بعنوان وب سرورمان قرار می گیرد. به بیان دقیقتر، بخش سرور، یک شي قابل صدا زدن (callable) را که بخش نرمافزار فراهم کرده است، صدا میزند. پس باید فایلی داشته باشیم که این شي درون آن قرار دارد. نام این فایل معمولا wsgi.py
است. در این جنگو نیز چنین است. یک فایل با اسم wsgi.py
در تمام پروژههای جنگو قرار دارد و نام شي موردنظر نیز application
میباشد. حال gunicorn چه میکند؟ gunicorn با صدا زدن این شي، هرکاری که برای ایجاد ارتباط بین پروژه و وبسرور نیاز باشد را بر عهده میگیرد و این ارتباط را ممکن میسازد.
ما در فایل بالا gunicorn را روی پورت 8000 فعال کردیم و بعدتر در کانفیگ nginx به آن گوش خواهیم داد. در بخش volumes نیز volume هایی را برای دائمی کردن داده ها و فایل های کانتینر تعریف کردیم تا پس از هر بار reset یا down شدن، داده های مهم ما از بین نروند. این قضیه بخصوص در باره دیتابیس اهمیت می یابد.
همچنین در پوشه اصلی پروژه یک فایل به نام env.prod. ایجاد کرده و بعنوان متغیرهای محیطی لینوکسی که روی کانتینر در حال اجراست معرفی کردیم. این کار به ما کمک می کند بعدتر در فایل setting.py از این متغیرهای محیطی استفاده کنیم و تنظیمات development و production را از طریق فایل های env. مختلف جدا کنیم. مشاهده یک نمونه از محتویات این فایل کمک کننده خواهد بود:
DEBUG=0 SECRET_KEY=... DJANGO_ALLOWED_HOSTS=example.ir www.example.ir 188.121.102.211 SIT_ID=4 CSRF_TRUSTED_ORIGINS=https://*.example.ir https://example.ir SQL_ENGINE=django.db.backends.postgresql SQL_DATABASE=my_djano_db SQL_USER=my_djano_user SQL_PASSWORD=1234 SQL_HOST=db #*service name in docker compose SQL_PORT=5432 #posegresdefault port DATABASE=postgres REDIS_HOST=redis://redis:6379/1
یک نکته بسیار مهم درباره فایل های .env که برای نگارنده این سطور نیز مسائل مختلفی را ایجاد کرده این است که در متغیر های مربوط به host دیتابیس حتما باید نام سرویسی که در doceker-compose.yml به سرویس دیتابیس اختصاص دادیم را قرار دهیم. نه نام یا id کانتینر ها را. چرا که id کانتینر ها هر چند مدت به طور خودکار تغییر پیدا می کند و در این صورت کانکشن دیتابیس ما به طورکلی از بین خواهد رفت چرا که به چیزی ارجاع داده شده بود که دیگر وجود ندارد!
همچنین به سرویس db نیز یک volume و یک فایل .env اختصاص دادیم. نمونه فایل .env دیتابیس:
POSTGRES_USER=my_djano_user POSTGRES_PASSWORD=1234 POSTGRES_DB=my_djano_db
این متغیرها بر اساس image رسمی داکر برای دیتابیس postgres در داکرهاب مقدار دهی شده اند. برای هر دیتابیس دیگری، مثلا mysql چنین توضیحاتی وجود دارد که باید بر اساس نیاز به آن رجوع کنید.
در باره سرویس redis که بخصوص بعنوان cache یا broker مربوط به celery استفاده می شود، برخی آپشن ها را به کار برده ایم که نیازی به توضیح نیست و می توانید با سرچ کردن متوجه شوید که چه هستند. صرفا هدف این بود که با نحوه اعمال این آپشن ها آشنا شوید.
و اما یکی از مهمترین قسمت های این فایل، سرویس nginx می باشد که در واقع وب سرور ماست. همانطور که مشاهده می کنید ما این سرویس را روی پورت 80 که پورت پیشفرض سرور است expose کرده ایم تا کاربر بدون وارد کردن پورت در دامنه به وبسایت ما دسترسی داشته باشد. همچنین گفته ایم که از Dockerfile موجود در پوشه nginx برای ساخت این image استفاده شود.
نکته مهم در باره سرویس nginx این است که فایل های static و media پروژه ما در نهایت باید توسط وب سرور nginx به اصطلاح serve شوند. بنابراین باید volume مربوط به این 2 دایرکتوری بین سرویس backend و nginx مشترک باشد.
در فایل docker-compose.yml بالا این آدرس ها بدین شکل می باشند:
- ./core/static:/home/app/web/static - ./core/media:/home/app/web/media
می توانستیم از named volume ها نیز استفاده کنیم و اتفاقا روش بهتری نیز بود! اما در اینجا برای سادگی از روش bind mount استفاده کردیم. در صورتی که کنجکاو هستید بد نیست در باره کلیدواژه های عنوان شده یک جست و جوی مختصر انجام دهید.
مرحله آخر کار، کانفیگ کردن فایل nginx.conf می باشد. بدین منظور چنین فایلی را با محتویات زیر در دایرکتوری nginx می سازیم:
upstream my-django-app { server backend:8000; } server { server_name example.ir; listen 80; location /static/ { alias /home/app/web/static/; } location /media/ { alias /home/app/web/media/; } location / { proxy_pass http://my-django-app; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header Host $host; proxy_redirect off; } }
در ابتدا یک upstream تعیین کردیم و پورتی که gunicorn روی آن اجرا می شد را به آن معرفی کردیم. "upstream" مشخص میکند که درخواستهای کاربر به چه سروری منتقل شود. در این مثال تمامی requestهای لوکیشینی که با / تمام میشود را به proxy_pass که مقدارش http://my-django-app است میفرستد و از طریق upstream که به نام my-django-app تعریفش کردیم کنترل میشود.
همچنین لازم به ذکر است که بلاک location ریکوئست ها به سرور را از طریق مسیرشان گروه بندی می کند. پس 2 لوکیشن هم برای serve کردن فایل های static و media تعریف میکنیم:
location /static/ { alias /home/app/web/static/; } location /media/ { alias /home/app/web/media/; }
در این کانفیگ ما ساده ترین حالت ممکن را انجام داده ایم، حال آنکه بسیاری کارهای دیگر نیز می توان و می بایست انجام می شدند. از جمله گواهی ssl نیز نیاز است. همچنین می توان از nginx به عنوان لودبالانسر نیز استفاده کرد که البته این مباحث این نوشتار را بیش از حد طولانی خواهند کرد.
در آینده ممکن است به طور مفصل و اختصاصی تری به مباحث مربوط به nginx بپردازیم. برای چگونگی دریافت گواهینامه ssl اما می توانید به این مقاله رجوع کنید.
در نهایت با زدن دستور docker compose up -d --build در پوشه اصلی پروژه، می توانیم کانتیرها را ساخته و سرویس هایمان را اجرا کنیم. برای اطمینان از درست اجرا شدن docker compose می توانید با زدن دستور زیر از وضعیت سرویس ها مطلع شوید:
docker compose ps
همچنین در صورت وجود هر گونه مشکلی با دستور زیر و مطالعه 100 خط پایانی فایل log از آن مشکل آگاه شویم:
docker compose logs -n 100
امیدوارم که این مطلب مفیده بوده باشد.