diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000000..75caec908a --- /dev/null +++ b/.dockerignore @@ -0,0 +1,3 @@ +.git +.gitignore +*.md \ No newline at end of file diff --git a/.github/workflows/production.yml b/.github/workflows/production.yml new file mode 100644 index 0000000000..f89ecdb167 --- /dev/null +++ b/.github/workflows/production.yml @@ -0,0 +1,63 @@ +name: Production deployment + +on: + release: + types: [published] + +env: + TAG: ${{ github.event.release.tag_name }} + STACK_FILE: docker/production.yml + REPOSITORY: website + +jobs: + build: + runs-on: ubuntu-latest + steps: + - name: Checkout the repo + uses: actions/checkout@v2 + + - name: Login to DockerHub + uses: docker/login-action@v1 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Build and push + uses: docker/build-push-action@v2 + with: + context: . + push: true + tags: ghcr.io/appwrite/website:${{ env.TAG }} + + deploy: + needs: build + runs-on: ubuntu-latest + steps: + - name: Execute SSH commands + uses: appleboy/ssh-action@master + with: + host: ${{ secrets.PRD_SSH_HOST }} + username: ${{ secrets.PRD_SSH_USERNAME }} + key: ${{ secrets.PRD_SSH_KEY }} + script: | + url="https://${{ github.actor }}:${{ secrets.GITHUB_TOKEN }}@github.com/appwrite/${{ env.REPOSITORY }}.git" + if ! git clone "${url}" "${{ env.REPOSITORY }}" 2>/dev/null && [ -d "${{ env.REPOSITORY }}" ] ; then + echo "Clone failed because the folder ${{ env.REPOSITORY }} exists" + fi + + cd ${{ env.REPOSITORY }} + git reset --hard HEAD + git remote set-url origin $url + git fetch origin + git checkout ${{ env.TAG }} + + rm -f .env + echo "_APP_VERSION=${{ env.TAG }}" >> .env + echo "_APP_DOMAIN=${{ secrets.PRD_APP_DOMAIN }}" >> .env + echo "_APP_SYSTEM_SECURITY_EMAIL_ADDRESS=${{ secrets.APP_SYSTEM_SECURITY_EMAIL_ADDRESS }}" >> .env + echo "SEMATEXT_TOKEN=${{ secrets.SEMATEXT_TOKEN }}" >> .env + + echo ${{ secrets.GH_REGISTRY_TOKEN }} | docker login ghcr.io --username ${{ github.actor }} --password-stdin + docker-compose -f ${{ env.STACK_FILE }} config + env $(cat .env | xargs) docker stack deploy --prune --resolve-image always --with-registry-auth -c ${{ env.STACK_FILE }} ${{ env.REPOSITORY }} \ No newline at end of file diff --git a/.github/workflows/staging.yml b/.github/workflows/staging.yml new file mode 100644 index 0000000000..f918ae7f2c --- /dev/null +++ b/.github/workflows/staging.yml @@ -0,0 +1,64 @@ +name: Staging deployment + +on: + push: + branches: + - main + +env: + TAG: ${{ github.sha }} + STACK_FILE: docker/stage.yml + REPOSITORY: website + +jobs: + build: + runs-on: ubuntu-latest + steps: + - name: Checkout the repo + uses: actions/checkout@v2 + + - name: Login to DockerHub + uses: docker/login-action@v1 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Build and push + uses: docker/build-push-action@v2 + with: + context: . + push: true + tags: ghcr.io/appwrite/website:${{ env.TAG }} + + deploy: + needs: build + runs-on: ubuntu-latest + steps: + - name: Execute SSH commands + uses: appleboy/ssh-action@master + with: + host: ${{ secrets.STG_SSH_HOST }} + username: ${{ secrets.STG_SSH_USERNAME }} + key: ${{ secrets.STG_SSH_KEY }} + script: | + url="https://${{ github.actor }}:${{ secrets.GITHUB_TOKEN }}@github.com/appwrite/${{ env.REPOSITORY }}.git" + if ! git clone "${url}" "${{ env.REPOSITORY }}" 2>/dev/null && [ -d "${{ env.REPOSITORY }}" ] ; then + echo "Clone failed because the folder ${{ env.REPOSITORY }} exists" + fi + + cd ${{ env.REPOSITORY }} + git reset --hard HEAD + git remote set-url origin $url + git fetch origin + git checkout ${{ env.TAG }} + + rm -f .env + echo "_APP_VERSION=${{ env.TAG }}" >> .env + echo "_APP_DOMAIN=${{ secrets.STG_APP_DOMAIN }}" >> .env + echo "_APP_SYSTEM_SECURITY_EMAIL_ADDRESS=${{ secrets.APP_SYSTEM_SECURITY_EMAIL_ADDRESS }}" >> .env + echo "SEMATEXT_TOKEN=${{ secrets.SEMATEXT_TOKEN }}" >> .env + + echo ${{ secrets.GH_REGISTRY_TOKEN }} | docker login ghcr.io --username ${{ github.actor }} --password-stdin + docker-compose -f ${{ env.STACK_FILE }} config + env $(cat .env | xargs) docker stack deploy --prune --resolve-image always --with-registry-auth -c ${{ env.STACK_FILE }} ${{ env.REPOSITORY }} \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000000..90bbb2d2eb --- /dev/null +++ b/Dockerfile @@ -0,0 +1,20 @@ +FROM node:20-bullseye AS build + +ENV PNPM_HOME="/pnpm" +ENV PATH="$PNPM_HOME:$PATH" + +WORKDIR /app +COPY . . + +RUN corepack enable +RUN --mount=type=cache,id=pnpm,target=/pnpm/store pnpm install +RUN NODE_OPTIONS=--max_old_space_size=4096 pnpm run build + +# Node alpine image to serve the generated static files +FROM node:20-alpine AS serve + +WORKDIR /app +COPY --from=build /app . + +EXPOSE 3000 +CMD [ "node", "build/index.js"] \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000000..f97748714c --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,47 @@ +version: '3.8' + +services: + traefik: + image: traefik:2.9 + command: + - --log.level=DEBUG + - --api.insecure=true + - --providers.docker=true + - --providers.docker.exposedByDefault=false + - --entrypoints.web.address=:80 + - --entrypoints.websecure.address=:443 + - --providers.docker.constraints=Label(`traefik.constraint-label-stack`,`homepage`) + - --accesslog=true + ports: + - 80:80 + - 8080:8080 + volumes: + - /letsencrypt:/letsencrypt + - /var/run/docker.sock:/var/run/docker.sock + networks: + - homepage + + homepage: + image: homepage-dev + build: + context: . + restart: always + networks: + - homepage + labels: + - traefik.enable=true + - traefik.constraint-label-stack=homepage + - traefik.docker.network=homepage + - traefik.http.services.homepage.loadbalancer.server.port=3000 + #http + - traefik.http.routers.homepage.entrypoints=web + - traefik.http.routers.homepage.rule=PathPrefix(`/`) + - traefik.http.routers.homepage.service=homepage + # https + - traefik.http.routers.homepage_secure.entrypoints=websecure + - traefik.http.routers.homepage_secure.rule=PathPrefix(`/`) + - traefik.http.routers.homepage_secure.service=homepage + - traefik.http.routers.homepage_secure.tls=true + +networks: + homepage: \ No newline at end of file diff --git a/docker/production.yml b/docker/production.yml new file mode 100644 index 0000000000..f585b145ad --- /dev/null +++ b/docker/production.yml @@ -0,0 +1,115 @@ +x-logging: &x-logging + logging: + driver: 'json-file' + options: + max-file: '5' + max-size: '20m' + +x-update-config: &x-update-config + update_config: + order: stop-first + failure_action: rollback + parallelism: 2 + delay: 5s + rollback_config: + failure_action: pause + monitor: 5s + parallelism: 2 + order: stop-first + +version: '3.8' + +services: + traefik: + image: traefik:2.9 + <<: *x-logging + command: + - --log.level=DEBUG + - --api.insecure=false + - --providers.docker=true + - --providers.docker.watch=true + - --providers.docker.swarmMode=true + - --providers.docker.exposedByDefault=false + - --entrypoints.web.address=:80 + - --entrypoints.websecure.address=:443 + - --entrypoints.web.http.redirections.entrypoint.to=websecure + - --entrypoints.web.http.redirections.entrypoint.scheme=https + - --providers.docker.constraints=Label(`traefik.constraint-label-stack`,`appwrite`) + - --certificatesresolvers.myresolver.acme.httpchallenge=true + - --certificatesresolvers.myresolver.acme.httpchallenge.entrypoint=web + - --certificatesresolvers.myresolver.acme.email=$_APP_SYSTEM_SECURITY_EMAIL_ADDRESS + - --certificatesresolvers.myresolver.acme.storage=/letsencrypt/${_APP_DOMAIN}.json + - --accesslog=true + ports: + - 80:80 + - 443:443 + volumes: + - /letsencrypt:/letsencrypt + - /var/run/docker.sock:/var/run/docker.sock + networks: + - cloud + deploy: + mode: global + <<: *x-update-config + placement: + constraints: + - node.role == manager + + server: + image: ghcr.io/appwrite/website:$_APP_VERSION + <<: *x-logging + networks: + - cloud + deploy: + <<: *x-update-config + mode: replicated + replicas: 6 + placement: + max_replicas_per_node: 2 + constraints: + - node.role == worker + preferences: + - spread: node.role == worker + labels: + - traefik.enable=true + - traefik.constraint-label-stack=appwrite + - traefik.http.services.appwrite_api.loadbalancer.server.port=3000 + #http + - traefik.http.routers.appwrite.entrypoints=web + - traefik.http.routers.appwrite.rule=Host(`$_APP_DOMAIN`) || Host(`www.$_APP_DOMAIN`) + - traefik.http.routers.appwrite.service=appwrite_api + # https + - traefik.http.routers.appwrite_secure.entrypoints=websecure + - traefik.http.routers.appwrite_secure.rule=Host(`$_APP_DOMAIN`) || Host(`www.$_APP_DOMAIN`) + - traefik.http.routers.appwrite_secure.service=appwrite_api + - traefik.http.routers.appwrite_secure.tls=true + - traefik.http.routers.appwrite_secure.tls.certresolver=myresolver + + janitor: + image: appwrite/docker-janitor + deploy: + mode: global + volumes: + - /var/run/docker.sock:/var/run/docker.sock + + sematext-agent: + image: sematext/agent:latest + environment: + REGION: EU + INFRA_TOKEN: $SEMATEXT_TOKEN + deploy: + mode: global + restart_policy: + condition: any + volumes: + - /:/hostfs:ro + - /etc/passwd:/etc/passwd:ro + - /etc/group:/etc/group:ro + - /sys:/host/sys:ro + - /dev:/hostfs/dev:ro + - /var/run:/var/run + - /sys/kernel/debug:/sys/kernel/debug + +networks: + cloud: + driver: overlay \ No newline at end of file diff --git a/docker/stage.yml b/docker/stage.yml new file mode 100644 index 0000000000..fff63e41fb --- /dev/null +++ b/docker/stage.yml @@ -0,0 +1,116 @@ +x-logging: &x-logging + logging: + driver: 'json-file' + options: + max-file: '5' + max-size: '20m' + +x-update-config: &x-update-config + update_config: + order: stop-first + failure_action: rollback + parallelism: 2 + delay: 5s + rollback_config: + failure_action: pause + monitor: 5s + parallelism: 2 + order: stop-first + +version: '3.8' + +services: + traefik: + image: traefik:2.9 + <<: *x-logging + command: + - --log.level=DEBUG + - --api.insecure=true + - --providers.docker=true + - --providers.docker.watch=true + - --providers.docker.swarmMode=true + - --providers.docker.exposedByDefault=false + - --entrypoints.web.address=:80 + - --entrypoints.websecure.address=:443 + - --entrypoints.web.http.redirections.entrypoint.to=websecure + - --entrypoints.web.http.redirections.entrypoint.scheme=https + - --providers.docker.constraints=Label(`traefik.constraint-label-stack`,`appwrite`) + - --certificatesresolvers.myresolver.acme.httpchallenge=true + - --certificatesresolvers.myresolver.acme.httpchallenge.entrypoint=web + - --certificatesresolvers.myresolver.acme.email=$_APP_SYSTEM_SECURITY_EMAIL_ADDRESS + - --certificatesresolvers.myresolver.acme.storage=/letsencrypt/${_APP_DOMAIN}.json + - --accesslog=true + ports: + - 80:80 + - 443:443 + - 8080:8080 + volumes: + - /letsencrypt:/letsencrypt + - /var/run/docker.sock:/var/run/docker.sock + networks: + - cloud + deploy: + mode: global + <<: *x-update-config + placement: + constraints: + - node.role == manager + + server: + image: ghcr.io/appwrite/website:$_APP_VERSION + <<: *x-logging + networks: + - cloud + deploy: + <<: *x-update-config + mode: replicated + replicas: 6 + placement: + max_replicas_per_node: 2 + constraints: + - node.role == worker + preferences: + - spread: node.role == worker + labels: + - traefik.enable=true + - traefik.constraint-label-stack=appwrite + - traefik.http.services.appwrite_api.loadbalancer.server.port=3000 + #http + - traefik.http.routers.appwrite.entrypoints=web + - traefik.http.routers.appwrite.rule=Host(`$_APP_DOMAIN`) || Host(`www.$_APP_DOMAIN`) + - traefik.http.routers.appwrite.service=appwrite_api + # https + - traefik.http.routers.appwrite_secure.entrypoints=websecure + - traefik.http.routers.appwrite_secure.rule=Host(`$_APP_DOMAIN`) || Host(`www.$_APP_DOMAIN`) + - traefik.http.routers.appwrite_secure.service=appwrite_api + - traefik.http.routers.appwrite_secure.tls=true + - traefik.http.routers.appwrite_secure.tls.certresolver=myresolver + + janitor: + image: appwrite/docker-janitor + deploy: + mode: global + volumes: + - /var/run/docker.sock:/var/run/docker.sock + + sematext-agent: + image: sematext/agent:latest + environment: + REGION: EU + INFRA_TOKEN: $SEMATEXT_TOKEN + deploy: + mode: global + restart_policy: + condition: any + volumes: + - /:/hostfs:ro + - /etc/passwd:/etc/passwd:ro + - /etc/group:/etc/group:ro + - /sys:/host/sys:ro + - /dev:/hostfs/dev:ro + - /var/run:/var/run + - /sys/kernel/debug:/sys/kernel/debug + +networks: + cloud: + driver: overlay \ No newline at end of file