diff --git a/.github/workflows/docker-build.yml b/.github/workflows/docker-build.yml new file mode 100644 index 00000000..54dc954f --- /dev/null +++ b/.github/workflows/docker-build.yml @@ -0,0 +1,64 @@ +name: Docker Image CI/CD + +on: + push: + branches: + - "main" + workflow_dispatch: + +jobs: + backend: + runs-on: ubuntu-latest + + steps: + - name: Check repository + uses: actions/checkout@v4 + + - name: Get date for tagging + id: date + run: echo "date=$(date +'%Y%m%d%H%M%S')" >> $GITHUB_ENV + + - name: Login to DockerHub + uses: docker/login-action@v3 + with: + username: ${{ vars.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + + - name: Build and push the Docker image + uses: docker/build-push-action@v5 + with: + context: . + file: ./docker/server/Dockerfile + push: true + tags: | + ${{ vars.DOCKERHUB_USERNAME }}/wajo-prod-server:latest + ${{ vars.DOCKERHUB_USERNAME }}/wajo-prod-server:${{ env.date }} + + frontend: + runs-on: ubuntu-latest + + steps: + - name: Check repository + uses: actions/checkout@v4 + + - name: Get date for tagging + id: date + run: echo "date=$(date +'%Y%m%d%H%M%S')" >> $GITHUB_ENV + + # Login + - name: Login to Docker Hub + uses: docker/login-action@v3 + with: + username: ${{ vars.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + + # Client Docker image + - name: Build and push the client Docker image + uses: docker/build-push-action@v5 + with: + context: . + file: ./docker/client/Dockerfile + push: true + tags: | + ${{ vars.DOCKERHUB_USERNAME }}/wajo-prod-client:latest + ${{ vars.DOCKERHUB_USERNAME }}/wajo-prod-client:${{ env.date }} diff --git a/.github/workflows/template-sync.yml b/.github/workflows/template-sync.yml index 202b4f67..03d35898 100644 --- a/.github/workflows/template-sync.yml +++ b/.github/workflows/template-sync.yml @@ -18,11 +18,11 @@ jobs: uses: actions/checkout@v4 with: # submodules: true - token: ${{ secrets.TEMPLATE_SYNC_TOKEN }} + token: ${{ secrets.GITHUB_TOKEN }} - name: actions-template-sync uses: AndreasAugustin/actions-template-sync@v2 with: - github_token: ${{ secrets.GITHUB_TOKEN }} + source_gh_token: ${{ secrets.GITHUB_TOKEN }} source_repo_path: codersforcauses/django-nextjs-template upstream_branch: main diff --git a/README.md b/README.md index 52d8cdc8..b8709dc0 100644 --- a/README.md +++ b/README.md @@ -36,3 +36,10 @@ If you modify anything in the `docker` folder, you need to add the `--build` fla ### Changing env vars Edit the `.env` file in the respective directory (client or server). + +### Production mode + +Use code below to start **Production** mode after modify the `.env` to `PRODUCTION` +```bash +docker compose -f docker-compose.prod.yml up -d --build +``` diff --git a/docker-compose.prod.yml b/docker-compose.prod.yml new file mode 100644 index 00000000..67a861ab --- /dev/null +++ b/docker-compose.prod.yml @@ -0,0 +1,62 @@ +services: + db: + image: postgres:16.4 + restart: unless-stopped + volumes: + - ${LOCAL_WORKSPACE_FOLDER:-.}/data/db:/var/lib/postgresql/data + healthcheck: + test: ["CMD-SHELL", "pg_isready -U postgres"] + interval: 3s + timeout: 3s + retries: 5 + env_file: ./server/.env + ports: + - 5432:5432 + server: + image: ${DOCKERHUB_USERNAME}/wajo-prod-server:latest + container_name: wajo_server + restart: unless-stopped + env_file: ./.env.server + entrypoint: /entrypoint.sh + volumes: + - ./opt/gunicorn-logs/:/var/log/gunicorn/ + - ./opt/django-logs/:/var/log/django/ + - ./opt/static_files/:/app/static_files + - ./opt/media/:/app/mediafiles/media + depends_on: + - db + client: + image: ${DOCKERHUB_USERNAME}/wajo-prod-client:latest + container_name: wajo-client + restart: unless-stopped + env_file: ./.env.client + ports: + - 3000:3000 + depends_on: + - server + + nginx: + image: nginx + container_name: wajo_nginx + restart: unless-stopped + ports: + - 80:3000 + - 443:3000 + - 3000:3000 + - 8000:8000 + volumes: + - ./custom.conf:/etc/nginx/conf.d/default.conf + - ./opt/static_files:/opt/static_files + - ./opt/media:/opt/media + - ./opt/nginx-logs/:/var/log/nginx/ + depends_on: + - client + - server + + watchtower: + image: containrrr/watchtower + container_name: wajo_watchtower + restart: unless-stopped + volumes: + - /var/run/docker.sock:/var/run/docker.sock + command: --interval 30 diff --git a/docker/client/Dockerfile b/docker/client/Dockerfile index e7492aa8..2e3f3efe 100644 --- a/docker/client/Dockerfile +++ b/docker/client/Dockerfile @@ -1,4 +1,4 @@ -FROM node:20-slim as development-stage +FROM node:20-slim as client-prod # SET WORKING DIRECTORY WORKDIR /app @@ -6,20 +6,15 @@ WORKDIR /app # Copy runtime script & make it executable COPY /docker/client/entrypoint.sh /entrypoint.sh -COPY ./client/package.json ./client/package-lock.json ./ - -# Install ALL Dependencies -RUN npm install +# Make the script executable +RUN chmod +x /entrypoint.sh # Copy Application code into a directory called `app` -COPY ./client /app +COPY ./client ./ -# ======================================== -# ---- Executed at Container Runtime ---- -# ======================================== +# Install dependencies without dev dependencies +RUN npm install --production -# CMD commands get executed at container runtime! -RUN chmod +x /entrypoint.sh -CMD ["/entrypoint.sh"] +RUN npm run build -# TODO: Production \ No newline at end of file +EXPOSE 3000 \ No newline at end of file diff --git a/docker/client/entrypoint.sh b/docker/client/entrypoint.sh index 0173c5c0..7f7ffae7 100644 --- a/docker/client/entrypoint.sh +++ b/docker/client/entrypoint.sh @@ -55,4 +55,11 @@ if [ "${APP_ENV^^}" = "DEVELOPMENT" ]; then echo "======= Starting inbuilt nextjs webserver ===================================================================" npm run dev exit +fi + +if [[ "${APP_ENV^^}" = "PRODUCTION" ]]; then + # npm install --production + echo " " + echo "======= Starting nextjs webserver ===================================================================" + npm run start fi \ No newline at end of file diff --git a/docker/nginx/custom.conf b/docker/nginx/custom.conf new file mode 100644 index 00000000..25b8bf3c --- /dev/null +++ b/docker/nginx/custom.conf @@ -0,0 +1,21 @@ +server { + listen 8000; + server_name localhost; + access_log /var/log/nginx/access.log; + error_log /var/log/nginx/error.log; + client_max_body_size 5M; + + location / { + proxy_pass http://server:8081; + proxy_set_header Host $host; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + } + + location /static/ { + alias /opt/static_files/; + } + + location /media/ { + alias /opt/media/; + } +} \ No newline at end of file diff --git a/docker/server/Dockerfile b/docker/server/Dockerfile index 3e886bde..b70527e2 100644 --- a/docker/server/Dockerfile +++ b/docker/server/Dockerfile @@ -11,11 +11,8 @@ WORKDIR /app COPY ./docker/server/entrypoint.sh /entrypoint.sh -COPY ./server/pyproject.toml ./server/poetry.lock ./ - -RUN poetry install - COPY ./server ./ -RUN chmod +x /entrypoint.sh -CMD ["/entrypoint.sh"] \ No newline at end of file +RUN poetry install --without dev + +RUN chmod +x /entrypoint.sh \ No newline at end of file diff --git a/docker/server/entrypoint.sh b/docker/server/entrypoint.sh index 5d36abfe..77cbb722 100644 --- a/docker/server/entrypoint.sh +++ b/docker/server/entrypoint.sh @@ -39,9 +39,13 @@ fi # Run Django/Gunicorn # =================== if [ "${APP_ENV^^}" = "PRODUCTION" ]; then + # Create file to hold django logs + printf "\n" && echo "Creating file to hold django logs" + echo "Running: mkdir -p /var/log/django && touch /var/log/django/django.log" + mkdir -p /var/log/django && touch /var/log/django/django.log # Run Gunicorn / Django printf "\n" && echo " Running Gunicorn / Django" - echo "Running: gunicorn api.wsgi -b 0.0.0.0:8000 --workers=6 --keep-alive 20 --log-file=- --log-level debug --access-logfile=/var/log/accesslogs/gunicorn --capture-output --timeout 50" - gunicorn api.wsgi -b 0.0.0.0:8000 --workers=6 --keep-alive 20 --log-file=- --log-level debug --access-logfile=/var/log/accesslogs/gunicorn --capture-output --timeout 50 + echo "Running: gunicorn api.wsgi -b 0.0.0.0:8081 --workers=3 --keep-alive 20 --log-level info --access-logfile=/var/log/gunicorn/access.log --error-logfile=/var/log/gunicorn/error.log --capture-output --timeout 50" + gunicorn api.wsgi -b 0.0.0.0:8081 --workers=3 --keep-alive 20 --log-level info --access-logfile=/var/log/gunicorn/access.log --error-logfile=/var/log/gunicorn/error.log --capture-output --timeout 50 fi \ No newline at end of file diff --git a/rundev.sh b/rundev.sh new file mode 100755 index 00000000..877d11b2 --- /dev/null +++ b/rundev.sh @@ -0,0 +1,2 @@ +#!/bin/bash +(cd server && ./dev.sh) & (cd client && npm run dev ) diff --git a/server/api/asgi.py b/server/api/asgi.py index 6e90b887..90426ca8 100644 --- a/server/api/asgi.py +++ b/server/api/asgi.py @@ -11,6 +11,6 @@ from django.core.asgi import get_asgi_application -os.environ.setdefault("DJANGO_SETTINGS_MODULE", "server.settings") +os.environ.setdefault("DJANGO_SETTINGS_MODULE", "api.settings") application = get_asgi_application() diff --git a/server/api/settings.py b/server/api/settings.py index f1e5f46d..846e6cd9 100644 --- a/server/api/settings.py +++ b/server/api/settings.py @@ -146,11 +146,11 @@ STATIC_URL = "/static/" # STATIC_ROOT is where the static files get copied to when "collectstatic" is run. -STATIC_ROOT = "static_files" +STATIC_ROOT = os.path.join(PROJECT_ROOT, "static_files") # This is where to _find_ static files when 'collectstatic' is run. # These files are then copied to the STATIC_ROOT location. -STATICFILES_DIRS = ("static",) +STATICFILES_DIRS = (os.path.join(PROJECT_ROOT, "static"),) # Default primary key field type # https://docs.djangoproject.com/en/5.0/ref/settings/#default-auto-field diff --git a/server/api/wsgi.py b/server/api/wsgi.py index af988b52..7fc622a1 100644 --- a/server/api/wsgi.py +++ b/server/api/wsgi.py @@ -11,6 +11,6 @@ from django.core.wsgi import get_wsgi_application -os.environ.setdefault("DJANGO_SETTINGS_MODULE", "server.settings") +os.environ.setdefault("DJANGO_SETTINGS_MODULE", "api.settings") application = get_wsgi_application()