From ea35f30bb7511376c1611b62a24e866d7f549358 Mon Sep 17 00:00:00 2001 From: Spencer Lepine <60903378+spencerlepine@users.noreply.github.com> Date: Fri, 25 Oct 2024 21:49:16 +0300 Subject: [PATCH] save progress - docker broken :( --- .dockerignore | 8 +++ .github/workflows/manual-deploy.yml | 43 ++++++++++++ COMMITS | 9 +++ README.md | 2 +- bootstrap.sh | 62 ++++++++++++++++ docker/Dockerfile | 53 ++++++++++++++ docker/docker-compose.dev.yml | 7 ++ docker/docker-compose.prod.yml | 105 ++++++++++++++++++++++++++++ package.json | 2 +- requests.md | 26 +++++++ yarn.lock | 8 +-- 11 files changed, 319 insertions(+), 6 deletions(-) create mode 100644 .dockerignore create mode 100644 .github/workflows/manual-deploy.yml create mode 100644 COMMITS create mode 100644 bootstrap.sh create mode 100644 docker/Dockerfile create mode 100644 docker/docker-compose.dev.yml create mode 100644 docker/docker-compose.prod.yml create mode 100644 requests.md diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..4c7db2f --- /dev/null +++ b/.dockerignore @@ -0,0 +1,8 @@ +Dockerfile +.dockerignore +node_modules +npm-debug.log +README.md +.next +docker +.git \ No newline at end of file diff --git a/.github/workflows/manual-deploy.yml b/.github/workflows/manual-deploy.yml new file mode 100644 index 0000000..66b4cc6 --- /dev/null +++ b/.github/workflows/manual-deploy.yml @@ -0,0 +1,43 @@ +# .github/workflows/manual-deploy.yml +# TODO_PRODUCTION + +name: Manually Deploy + +on: + push: + branches: + - main + +jobs: + #bootstrap: + # run bootstrap.ssh? + # PUBLIC_KEY="your_ssh_public_key_here" DOMAIN="your_domain_here" bash bootstrap.sh + + deploy: + runs-on: ubuntu-latest + permissions: + contents: read + + steps: + - name: Re-deploy, update secrets + env: + # secure: only stored in memory during the remote-ssh session + API_KEY: ${{ secrets.API_KEY }} + DB_PASSWORD: ${{ secrets.DB_PASSWORD }} + run: | + ssh user@your-vm-ip << 'EOF' + # Authenticate with GitHub CLI using the passed PAT + echo "$GITHUB_TOKEN" | gh auth login --with-token + + # Create the .env file with secrets + { + echo "API_KEY=$API_KEY" + echo "DB_PASSWORD=$DB_PASSWORD" + } > .env + + # Run docker-compose with the .env file + docker-compose --env-file .env up -d + + # Remove the .env file immediately + rm .env + EOF \ No newline at end of file diff --git a/COMMITS b/COMMITS new file mode 100644 index 0000000..ba8b01d --- /dev/null +++ b/COMMITS @@ -0,0 +1,9 @@ +(update the changelog each time) +feat: e2e stripe checkout functionality +feat: printify order integration +feat: passwordless email login +cleanup: retry logic, error handling, logging + +checklist: +https://nextjs.org/docs/app/building-your-application/deploying/production-checklist + diff --git a/README.md b/README.md index 098319d..d03d4d9 100644 --- a/README.md +++ b/README.md @@ -94,7 +94,7 @@ stripe listen --forward-to localhost:3000/api/v1/webhook/checkout # *open separate terminal* cp .env.template .env.development -docker-compose -f ./docker/development/docker-compose.yml --env-file .env.development up -d +docker-compose -f ./docker/docker-compose.dev.yml --env-file .env.development up --no-deps --build # visit http://locahost:3000 ``` diff --git a/bootstrap.sh b/bootstrap.sh new file mode 100644 index 0000000..f709934 --- /dev/null +++ b/bootstrap.sh @@ -0,0 +1,62 @@ +#!/bin/bash + +# bootstrap.sh +# script to install/configure software on ubuntu LTS virtual machine +# TODO_PRODUCTION - test this ubuntu LTS + +# Check if PUBLIC_KEY is set +if [ -z "$PUBLIC_KEY" ]; then + echo "Error: PUBLIC_KEY environment variable is not set." + exit 1 +fi + +# Check if DOMAIN is set +if [ -z "$DOMAIN" ]; then + echo "Error: DOMAIN environment variable is not set." + exit 1 +fi + +# Check if EMAIL is set +if [ -z "$EMAIL" ]; then + echo "Error: EMAIL environment variable is not set." + exit 1 +fi + +# Update and install necessary packages +apt update && apt upgrade -y +apt install -y software-properties-common + +# Install Docker +apt install -y docker.io +systemctl enable docker +systemctl start docker + +# Install Docker Compose +DOCKER_COMPOSE_VERSION="v2.16.0" # Change to the desired version +curl -SL "https://github.com/docker/compose/releases/download/$DOCKER_COMPOSE_VERSION/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose +chmod +x /usr/local/bin/docker-compose + +# Configure UFW +ufw allow OpenSSH +ufw enable + +# Install Certbot and Nginx +apt install -y certbot + +# Obtain the SSL certificate using standalone mode +certbot certonly --standalone -d "$DOMAIN" --non-interactive --agree-tos --email "$EMAIL" + + +# Disable password authentication +sed -i 's/#PasswordAuthentication yes/PasswordAuthentication no/' /etc/ssh/sshd_config +systemctl restart ssh + +# Add SSH public key +mkdir -p ~/.ssh +echo "$PUBLIC_KEY" >> ~/.ssh/authorized_keys +chmod 600 ~/.ssh/authorized_keys + +# Restart UFW to apply changes +systemctl restart ufw + +echo "Bootstrap script completed successfully." \ No newline at end of file diff --git a/docker/Dockerfile b/docker/Dockerfile new file mode 100644 index 0000000..c27063f --- /dev/null +++ b/docker/Dockerfile @@ -0,0 +1,53 @@ +FROM node:20.16-alpine3.19 AS base + +# 1. Install dependencies only when needed +FROM base AS deps +# Check https://github.com/nodejs/docker-node/tree/b4117f9333da4138b03a546ec926ef50a31506c3#nodealpine to understand why libc6-compat might be needed. +RUN apk add --no-cache libc6-compat + +WORKDIR /app + +# Install dependencies based on the preferred package manager +COPY package.json yarn.lock* package-lock.json* pnpm-lock.yaml* ./ +RUN \ + if [ -f yarn.lock ]; then yarn --frozen-lockfile; \ + elif [ -f package-lock.json ]; then npm ci; \ + elif [ -f pnpm-lock.yaml ]; then corepack enable pnpm && pnpm i; \ + else echo "Lockfile not found." && exit 1; \ + fi + +# 2. Rebuild the source code only when needed +FROM base AS builder +WORKDIR /app +COPY --from=deps /app/node_modules ./node_modules +COPY . . + +# Use the corresponding env file for each environment. +COPY .env.development .env.production +RUN npm run build + +# 3. Production image, copy all the files and run next +FROM base AS runner +WORKDIR /app + +ENV NODE_ENV=production + +RUN addgroup -g 1001 -S nodejs +RUN adduser -S nextjs -u 1001 + +COPY --from=builder /app/public ./public + +# Automatically leverage output traces to reduce image size +# https://nextjs.org/docs/advanced-features/output-file-tracing +COPY --from=builder --chown=nextjs:nodejs /app/.next ./.next +COPY --from=builder /app/node_modules ./node_modules +COPY --from=builder /app/package.json ./package.json +COPY --from=builder /app/public ./public + +USER nextjs + +EXPOSE 3000 + +ENV PORT=3000 + +CMD npm start \ No newline at end of file diff --git a/docker/docker-compose.dev.yml b/docker/docker-compose.dev.yml new file mode 100644 index 0000000..d164fa5 --- /dev/null +++ b/docker/docker-compose.dev.yml @@ -0,0 +1,7 @@ +services: + nextjs-app-local: + build: + context: ../ + dockerfile: docker/Dockerfile + ports: + - "3000:3000" \ No newline at end of file diff --git a/docker/docker-compose.prod.yml b/docker/docker-compose.prod.yml new file mode 100644 index 0000000..1f6f84f --- /dev/null +++ b/docker/docker-compose.prod.yml @@ -0,0 +1,105 @@ +# TODO_PRODUCTION + +services: + watchtower: + image: containrrr/watchtower + command: + - "--label-enable" + - "--interval" + - "30" + - "--rolling-restart" + volumes: + - /var/run/docker.sock:/var/run/docker.sock + reverse-proxy: + image: traefik:v3.1 + command: + - "--log.level=ERROR" + - "--accesslog=true" + - "--providers.docker" + - "--providers.docker.exposedbydefault=false" + - "--entryPoints.websecure.address=:443" + - "--certificatesresolvers.myresolver.acme.tlschallenge=true" + - "--certificatesresolvers.myresolver.acme.email=elliott@zenful.cloud" + - "--certificatesresolvers.myresolver.acme.storage=/letsencrypt/acme.json" + - "--entrypoints.web.address=:80" + - "--entrypoints.web.http.redirections.entrypoint.to=websecure" + - "--entrypoints.web.http.redirections.entrypoint.scheme=https" + - "--entryPoints.web.forwardedHeaders.insecure" + - "--entryPoints.websecure.forwardedHeaders.insecure" + ports: + - "80:80" + - "443:443" + volumes: + - letsencrypt:/letsencrypt + - /var/run/docker.sock:/var/run/docker.sock + nextjs-app: + image: ghcr.io/spencerlepine/nextjs-app:prod + labels: + - "traefik.enable=true" + - "traefik.http.middlewares.nextjs-app-ratelimit.ratelimit.average=20" + - "traefik.http.routers.nextjs-app.rule=Host(`zenful.cloud`) && !Method(`POST`)" + - "traefik.http.routers.nextjs-app.entrypoints=websecure" + - "traefik.http.routers.nextjs-app.tls.certresolver=myresolver" + - "traefik.http.routers.nextjs-app.middlewares=nextjs-app-ratelimit" + # Define separate router for POST methods + - "traefik.http.middlewares.nextjs-app-ratelimit-post.ratelimit.average=1" + - "traefik.http.middlewares.nextjs-app-ratelimit-post.ratelimit.period=1m" + - "traefik.http.routers.nextjs-app-post.rule=Host(`zenful.cloud`) && Method(`POST`)" + - "traefik.http.routers.nextjs-app-post.middlewares=nextjs-app-ratelimit-post" + - "traefik.http.routers.nextjs-app-post.entrypoints=websecure" + - "traefik.http.routers.nextjs-app-post.tls.certresolver=myresolver" + # Proxy + - "traefik.http.routers.proxy.rule=Host(`proxy.dreamsofcode.io`)" + - "traefik.http.routers.proxy.entrypoints=websecure" + - "traefik.http.routers.proxy.tls.certresolver=myresolver" + # Enable watchtower + - "com.centurylinklabs.watchtower.enable=true" + environment: + - POSTGRES_HOST=db + - POSTGRES_USER=postgres + - POSTGRES_DB=nextjs-app + - POSTGRES_PORT=5432 + - POSTGRES_SSLMODE=disable + - POSTGRES_PASSWORD=${DATABASE_PASSWORD} + deploy: + mode: replicated + replicas: 3 + restart: always + depends_on: + db: + condition: service_healthy + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost:3000"] + interval: 1m30s + timeout: 10s + retries: 3 + start_period: 40s + db: + image: postgres + restart: always + user: postgres + volumes: + - db-data:/var/lib/postgresql/data + environment: + - POSTGRES_DB=nextjs-app + - POSTGRES_PASSWORD=${DATABASE_PASSWORD} + expose: + - 5432 + healthcheck: + test: [ "CMD", "pg_isready" ] + interval: 10s + timeout: 5s + retries: 5 + + dragonfly: + image: 'docker.dragonflydb.io/dragonflydb/dragonfly' + ulimits: + memlock: -1 + network_mode: "host" + volumes: + - dragonflydata:/data + +volumes: + db-data: + letsencrypt: + dragonflydata: diff --git a/package.json b/package.json index 1ace801..e3ca4aa 100644 --- a/package.json +++ b/package.json @@ -24,7 +24,7 @@ "dependencies": { "fuse.js": "^7.0.0", "next": "14.2.11", - "printify-sdk-js": "^1.0.1", + "printify-sdk-js": "1.0.2", "react": "^18", "react-dom": "^18", "sharp": "^0.33.5", diff --git a/requests.md b/requests.md new file mode 100644 index 0000000..1f4f1d5 --- /dev/null +++ b/requests.md @@ -0,0 +1,26 @@ +curl -X GET https://api.printify.com/v1/shops.json --header "Authorization: Bearer $PRINTIFY_API_TOKEN" [{"id":1234567,"title":"StoreName Etsy","sales_channel":"etsy"}] + +curl -X GET https://api.printify.com/v1/catalog/blueprints/1268/print_providers/1/variants.json --header "Authorization: Bearer $PRINTIFY_API_TOKEN" + +curl -X GET https://api.printify.com/v1/catalog/blueprints/1268/print_providers.json --header "Authorization: Bearer $PRINTIFY_API_TOKEN" + +curl -X GET https://api.printify.com/v1/catalog/blueprints/1268/print_providers/215/variants.json --header "Authorization: Bearer $PRINTIFY_API_TOKEN" {"id":215,"title":"Stickers & +Posters","variants":[ {"id":95743,"title":"3\" x 4\" \/ Kiss-Cut \/ Satin","options":{"size":"3\" x +4\"","shape":"Kiss-Cut","paper":"Satin"},"placeholders":[{"position":"front","height":800,"width":600}]}, {"id":95744,"title":"4\" x 6\" \/ Kiss-Cut \/ +Satin","options":{"size":"4\" x 6\"","shape":"Kiss-Cut","paper":"Satin"},"placeholders":[{"position":"front","height":1200,"width":800}]}, {"id":95745,"title":"6\" x 8\" \/ +Kiss-Cut \/ Satin","options":{"size":"6\" x 8\"","shape":"Kiss-Cut","paper":"Satin"},"placeholders":[{"position":"front","height":1600,"width":1200}]}, {"id":95746,"title":"8\" x +10\" \/ Kiss-Cut \/ Satin","options":{"size":"8\" x 10\"","shape":"Kiss-Cut","paper":"Satin"},"placeholders":[{"position":"front","height":2000,"width":1600}]} ] } + +curl -X GET https://api.printify.com/v1/catalog/blueprints/1268/print_providers/215/shipping.json --header "Authorization: Bearer $PRINTIFY_API_TOKEN" { "handling_time": { "value": +10, "unit": "day" }, "profiles": [ { "variant_ids": [95743, 95744, 95745, 95746], "first_item": { "cost": 469, "currency": "USD" }, "additional_items": { "cost": 9, "currency": +"USD" }, "countries": ["US"] }, { "variant_ids": [95743, 95744, 95745, 95746], "first_item": { "cost": 839, "currency": "USD" }, "additional_items": { "cost": 49, "currency": "USD" +}, "countries": ["CA"] }, { "variant_ids": [95743, 95744, 95745, 95746], "first_item": { "cost": 1049, "currency": "USD" }, "additional_items": { "cost": 59, "currency": "USD" }, +"countries": ["REST_OF_THE_WORLD"] } ] } + +curl -X https://api.printify.com/v1/shops/{shop_id}/orders.json --header "Authorization: Bearer $PRINTIFY_API_TOKEN" + +{ "external_id": "2750e210-39bb-11e9-a503-452618153e4a", "label": "00012", "line_items": [ { "product_id": "5bfd0b66a342bcc9b5563216", "variant_id": 17887, "quantity": 1 } ], +"shipping_method": 1, "is_printify_express": false, "is_economy_shipping": false, "send_shipping_notification": false, "address_to": { "first_name": "John", "last_name": "Smith", +"email": "example@msn.com", "phone": "0574 69 21 90", "country": "BE", "region": "", "address1": "ExampleBaan 121", "address2": "45", "city": "Retie", "zip": "2470" } } + +curl -X GET https://api.printify.com/v1/shops/{shop_id}/orders/18739212.4/send_to_production.json --header "Authorization: Bearer $PRINTIFY_API_TOKEN" diff --git a/yarn.lock b/yarn.lock index 2faf44e..bc461b9 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2763,10 +2763,10 @@ prettier@2.8.8: resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.8.8.tgz#e8c5d7e98a4305ffe3de2e1fc4aca1a71c28b1da" integrity sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q== -printify-sdk-js@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/printify-sdk-js/-/printify-sdk-js-1.0.1.tgz#38b6f799050f02dc70b89730f6542ae51211e1cc" - integrity sha512-4eE6tGzkHspT7R7zSWY4eD8AVQE0+qB+rZmRirIcbOQEDtO0A7zr01QlBWNMs1zFdKe2E5jB5nxz8EQoQSe01Q== +printify-sdk-js@1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/printify-sdk-js/-/printify-sdk-js-1.0.2.tgz#497432b72021ee693ad241e7f5a2af771548eab6" + integrity sha512-wOS0hdIYqEhuiX782FcKTc4bnS1IEJeiA3WZUXSfrilWZlw3+nO1OXcfR9x0v6e76KplF2/2EHwgGMX9PzPs8Q== prop-types@^15.7.2, prop-types@^15.8.1: version "15.8.1"