From 52ab3b351bac47980969978b98edea1aa637c587 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=A9my=20Denquin?= Date: Wed, 12 Feb 2025 17:25:22 +0100 Subject: [PATCH] feat(docker): Single docker image (#464) * add a new docker image * add api * add pg, redis and foreman * add ga * tag with latest * update api * right tag on submodules * use one run --- .github/workflows/release-docker-image.yml | 116 +++++++++++++++++++++ docker/Dockerfile | 67 ++++++++++++ docker/Procfile | 3 + docker/README.md | 37 +++++++ docker/nginx.conf | 12 +++ docker/redis.conf | 75 +++++++++++++ docker/runner.sh | 72 +++++++++++++ scripts/bootstrap.sh | 7 ++ scripts/pg-init-scripts/bootstrap.sh | 7 ++ 9 files changed, 396 insertions(+) create mode 100644 .github/workflows/release-docker-image.yml create mode 100644 docker/Dockerfile create mode 100644 docker/Procfile create mode 100644 docker/README.md create mode 100644 docker/nginx.conf create mode 100644 docker/redis.conf create mode 100755 docker/runner.sh create mode 100755 scripts/bootstrap.sh create mode 100644 scripts/pg-init-scripts/bootstrap.sh diff --git a/.github/workflows/release-docker-image.yml b/.github/workflows/release-docker-image.yml new file mode 100644 index 000000000..2c56a9123 --- /dev/null +++ b/.github/workflows/release-docker-image.yml @@ -0,0 +1,116 @@ +name: "Release Single Docker Image" +on: + release: + types: [released] + workflow_dispatch: + inputs: + version: + description: Version + required: true +env: + REGISTRY_IMAGE: getlago/lago +jobs: + build-single-docker-image: + strategy: + matrix: + platform: + - version: linux/amd64 + runner: linux/amd64 + - version: linux/arm64 + runner: linux-arm64 + name: Build ${{ matrix.platform.version }} Image + runs-on: ${{ matrix.platform.runner }} + steps: + - name: Prepare + run: | + platform=${{ matrix.platform.version }} + echo "PLATFORM_PAIR=${platform//\//-}" >> $GITHUB_ENV + - uses: actions/checkout@v4 + - name: Docker Meta + id: meta + uses: docker/metadata-action@v5 + with: + images: ${{ env.REGISTRY_IMAGE }} + tags: | + type=raw,value=${{ github.event_name == 'release' && github.event.release.tag_name || github.event.inputs.version }} + type=raw,value=latest + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + with: + version: latest + - name: Log In to Docker Hub + uses: docker/login-action@v3 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_PASSWORD }} + - name: Add version into docker image + id: add_version + run: | + echo "${{ github.event_name == 'release' && github.event.release.tag_name || github.event.inputs.version }}" > LAGO_VERSION + - name: Build and push Docker image + uses: docker/build-push-action@v6 + id: build + with: + context: . + file: ./docker/Dockerfile + platforms: ${{ matrix.platform.version }} + labels: ${{ steps.meta.outputs.labels }} + outputs: type=image,name=${{ env.REGISTRY_IMAGE }},push-by-digest=true,name-canonical=true,push=true + build-args: | + SEGMENT_WRITE_KEY=${{ secrets.SEGMENT_WRITE_KEY }} + - name: Export Digest + run: | + mkdir -p ./_tmp/${{ github.run_id }}/${{ github.run_attempt }}/digests + digest="${{ steps.build.outputs.digest }}" + touch "./_tmp/${{ github.run_id }}/${{ github.run_attempt }}/digests/${digest#sha256:}" + - name: Upload Digest + uses: actions/upload-artifact@v4 + with: + name: digests-${{ env.PLATFORM_PAIR }} + path: ./_tmp/${{ github.run_id }}/${{ github.run_attempt }}/digests/* + if-no-files-found: error + retention-days: 1 + - name: Clean up + if: always() + run: | + [ -e ./_tmp/${{ github.run_id }}/${{ github.run_attempt }}/digests ] && \ + rm -rf ./_tmp/${{ github.run_id }}/${{ github.run_attempt }}/digests + merge: + name: Merge Images + runs-on: lago-runner + needs: [build-images] + steps: + - name: Download Digests + uses: actions/download-artifact@v4 + with: + path: ./_tmp/${{ github.run_id}}/${{ github.run_attempt }}/digests + pattern: digests-* + merge-multiple: true + - name: Docker meta + id: meta + uses: docker/metadata-action@v5 + with: + images: ${{ env.REGISTRY_IMAGE }} + tags: | + type=raw,value=${{ github.event_name == 'release' && github.event.release.tag_name || github.event.inputs.version }} + type=raw,value=latest + - name: Set up Docker buildx + uses: docker/setup-buildx-action@v3 + - name: Log in to Docker Hub + uses: docker/login-action@v3 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_PASSWORD }} + - name: Create manifest and push + working-directory: ./_tmp/${{ github.run_id }}/${{ github.run_attempt}}/digests + run: | + docker buildx imagetools create $(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \ + $(printf '${{ env.REGISTRY_IMAGE }}@sha256:%s ' *) + - name: Inspect Image + run: | + docker buildx imagetools inspect ${{ env.REGISTRY_IMAGE }}:${{ steps.meta.outputs.version }} + - name: Clean up + if: always() + run: | + [ -e ./_tmp/${{ github.run_id }}/${{ github.run_attempt }}/digests ] && \ + rm -rf ./_tmp/${{ github.run_id }}/${{ github.run_attempt }}/digests diff --git a/docker/Dockerfile b/docker/Dockerfile new file mode 100644 index 000000000..d033c99a7 --- /dev/null +++ b/docker/Dockerfile @@ -0,0 +1,67 @@ +ARG NODE_VERSION=20 +ARG RUBY_VERSION=3.3.6 + +# Front Build +FROM node:$NODE_VERSION-alpine AS front_build + +WORKDIR /app + +COPY ./front/ . + +RUN apk add python3 build-base +RUN yarn && yarn build && npm prune --omit=dev + +# API Build +FROM ruby:$RUBY_VERSION-slim AS api_build + +ENV BUNDLER_VERSION='2.5.5' +ENV PATH="$PATH:/root/.cargo/bin/" + +WORKDIR /app + +RUN apt-get update && apt-get upgrade -y && \ + apt-get install nodejs curl build-essential git pkg-config libpq-dev libclang-dev curl -y && \ + curl https://sh.rustup.rs -sSf | bash -s -- -y + +COPY ./api/Gemfile /app/Gemfile +COPY ./api/Gemfile.lock /app/Gemfile.lock + +RUN gem install bundler --no-document -v '2.5.5' && \ + gem install foreman && \ + bundle config build.nokogiri --use-system-libraries &&\ + bundle install --jobs=3 --retry=3 --without development test + +# Final Image +FROM ruby:$RUBY_VERSION-slim + +WORKDIR /app + +RUN apt-get update && apt-get upgrade -y && \ + apt-get install curl ca-certificates gnupg software-properties-common -y && \ + curl -fsSL https://postgresql.org/media/keys/ACCC4CF8.asc | gpg --dearmor | tee /usr/share/keyrings/postgresql-archive-keyring.gpg > /dev/null && \ + echo deb [arch=amd64,arm64,ppc64e1 signed-by=/usr/share/keyrings/postgresql.gpg] http://apt.postgresql.org/pub/repos/apt $(lsb_release -cs)-pgdg main | tee /etc/ap && \ + curl -fsSL https://packages.redis.io/gpg | gpg --dearmor -o /usr/share/keyrings/redis-archive-keyring.gpg && \ + chmod 644 /usr/share/keyrings/redis-archive-keyring.gpg &&\ + echo "deb [signed-by=/usr/share/keyrings/redis-archive-keyring.gpg] https://packages.redis.io/deb $(lsb_release -cs) main" | tee /etc/apt/sources.list.d/redis.list && \ + apt-get update && \ + apt-get install nginx xz-utils git libpq-dev postgresql-15 redis -y && \ + apt-get remove software-properties-common apt-transport-https -y + +COPY ./docker/nginx.conf /etc/nginx/sites-enabled/default +COPY ./docker/redis.conf /etc/redis/redis.conf + +COPY --from=front_build /app/dist /app/front +COPY --from=api_build /usr/local/bundle/ /usr/local/bundle + +COPY ./front/.env.sh ./front/.env.sh +COPY ./api ./api +COPY ./docker/Procfile ./api/Procfile +COPY ./docker/runner.sh ./runner.sh + +ENV SEGMENT_WRITE_KEY=$SEGMENT_WRITE_KEY + +EXPOSE 80 +EXPOSE 3000 +VOLUME /data + +ENTRYPOINT ["./runner.sh"] diff --git a/docker/Procfile b/docker/Procfile new file mode 100644 index 000000000..5930a8f19 --- /dev/null +++ b/docker/Procfile @@ -0,0 +1,3 @@ +web: bundle exec rails s -b :: -p 3000 +worker: bundle exec sidekiq -C config/sidekiq/sidekiq.yml +clock: bundle exec clockwork ./clock.rb diff --git a/docker/README.md b/docker/README.md new file mode 100644 index 000000000..a7ab68007 --- /dev/null +++ b/docker/README.md @@ -0,0 +1,37 @@ +# Lago Docker Image + +This is the official one docker image for Lago. +We do not recommend to use it in production for heavy usage, please check the `docker-compose` file in the root of the repository +or our [helm chart](https://github.com/getlago/lago-helm-charts) for a more robust deployment. + +## Features + +This docker image embed everything to run Lago with just one command line to ease the deployment. +Here are the services that are running into the container : +- PostgreSQL +- Redis +- Lago UI +- Lago API +- Lago Worker +- Lago Clock + +## Get Started + +```bash +docker run -d --name lago-p 80:80 -p 3000:3000 getlago/lago:latest +``` + +## Storage + +The container is using a volume to store the data, you can mount it to your host to keep the data safe. +You can find many folders for each services in the `/data` folder. + +## Logs + +Database Logs (creation, migration) are stored in the `/data/db.log` file. +Applicative logs are streamed to the standard output. + +## Contributing + +This docker image is a work in progress, this does not provide a lot of features yet (ei: external database configuration). +Feel free to open issues or PRs to improve it or ask for new features. diff --git a/docker/nginx.conf b/docker/nginx.conf new file mode 100644 index 000000000..a32453b79 --- /dev/null +++ b/docker/nginx.conf @@ -0,0 +1,12 @@ +server { + listen 80; + listen [::]:80; + + location / { + root /app/front; + index index.html index.htm; + try_files $uri $uri/ /index.html =404; + } + + include /etc/nginx/extra-conf.d/*.conf; +} diff --git a/docker/redis.conf b/docker/redis.conf new file mode 100644 index 000000000..1e1f0637c --- /dev/null +++ b/docker/redis.conf @@ -0,0 +1,75 @@ +bind 127.0.0.1 -::1 +protected-mode yes +port 6379 +tcp-backlog 511 +timeout 0 +tcp-keepalive 300 +daemonize yes +supervised auto +pidfile /run/redis/redis-server.pid +loglevel notice +logfile /var/log/redis/redis-server.log +databases 16 +always-show-logo no +set-proc-title yes +proc-title-template "{title} {listen-addr} {server-mode}" +locale-collate "" +stop-writes-on-bgsave-error yes +rdbcompression yes +rdbchecksum yes +dbfilename dump.rdb +rdb-del-sync-files no +dir /data/redis +replica-serve-stale-data yes +replica-read-only yes +repl-diskless-sync yes +repl-diskless-sync-delay 5 +repl-diskless-sync-max-replicas 0 +repl-diskless-load disabled +repl-disable-tcp-nodelay no +replica-priority 100 +acllog-max-len 128 +lazyfree-lazy-eviction no +lazyfree-lazy-expire no +lazyfree-lazy-server-del no +replica-lazy-flush no +lazyfree-lazy-user-del no +lazyfree-lazy-user-flush no +oom-score-adj no +oom-score-adj-values 0 200 800 +disable-thp yes +appendonly yes +appendfilename "appendonly.aof" +appenddirname "appendonlydir" +appendfsync everysec +no-appendfsync-on-rewrite no +auto-aof-rewrite-percentage 100 +auto-aof-rewrite-min-size 64mb +aof-load-truncated yes +aof-use-rdb-preamble yes +aof-timestamp-enabled no +slowlog-log-slower-than 10000 +slowlog-max-len 128 +latency-monitor-threshold 0 +notify-keyspace-events "" +hash-max-listpack-entries 512 +hash-max-listpack-value 64 +list-max-listpack-size -2 +list-compress-depth 0 +set-max-intset-entries 512 +set-max-listpack-entries 128 +set-max-listpack-value 64 +zset-max-listpack-entries 128 +zset-max-listpack-value 64 +hll-sparse-max-bytes 3000 +stream-node-max-bytes 4096 +stream-node-max-entries 100 +activerehashing yes +client-output-buffer-limit normal 0 0 0 +client-output-buffer-limit replica 256mb 64mb 60 +client-output-buffer-limit pubsub 32mb 8mb 60 +hz 10 +dynamic-hz yes +aof-rewrite-incremental-fsync yes +rdb-save-incremental-fsync yes +jemalloc-bg-thread yes diff --git a/docker/runner.sh b/docker/runner.sh new file mode 100755 index 000000000..0365f3c23 --- /dev/null +++ b/docker/runner.sh @@ -0,0 +1,72 @@ +#!/bin/bash + +declare -A ENV_VARS=( + [POSTGRES_PASSWORD]=$(openssl rand -hex 16) + [SECRET_KEY_BASE]=$(openssl rand -base64 16) + [LAGO_RSA_PRIVATE_KEY]=$(openssl genrsa 2048 | base64 | tr -d '\n') + [REDIS_URL]="redis://localhost:6379/0" + [LAGO_FRONT_URL]="http://localhost" + [LAGO_API_URL]="http://localhost:3000" + [API_URL]="http://localhost:3000" + [APP_ENV]="production" +) + +if [ -f "/data/.env" ]; then + for LINE in $(cat /data/.env); do export $LINE; done +fi + +# Configure data directories +if [ -z "${DATA_DIR}" ]; then + export DATA_DIR=/data + mkdir -p ${DATA_DIR} + mkdir -p ${DATA_DIR}/redis + chown redis:redis ${DATA_DIR}/redis + mkdir -p ${DATA_DIR}/postgresql + touch ${DATA_DIR}/db.log + touch ${DATA_DIR}/.env + echo "DATA_DIR=${DATA_DIR}" >> ${DATA_DIR}/.env +fi + +# Configure Redis +sed -i "s#DATA_DIR#${DATA_DIR}#g" /etc/redis/redis.conf + +# Configure PG +export PGDATA="${DATA_DIR}/postgresql" +export PGPORT=5432 + +# Start Redis, PG and Nginx +service redis-server restart >> /dev/null +service postgresql restart >> /dev/null +service nginx restart >> /dev/null + +# Prepare Environment +# Defaulting values +for VAR in "${!ENV_VARS[@]}"; do + if [ -z "${!VAR}" ]; then + export $VAR=${ENV_VARS[$VAR]} + echo "$VAR=${ENV_VARS[$VAR]}" >> ${DATA_DIR}/.env + fi +done + +if [ -z "${DATABASE_URL}" ]; then + export DATABASE_URL=postgresql://lago:$POSTGRES_PASSWORD@localhost:5432/lago + echo "DATABASE_URL=${DATABASE_URL}" >> ${DATA_DIR}/.env +fi + +# Prepare Front Environment +cd ./front +bash -c ./.env.sh +cd .. + +export RAILS_ENV=production + +# Create DB User +su -c "psql -tc \"SELECT 1 FROM pg_user WHERE usename = 'lago';\" | grep -q 1 || psql -c \"CREATE ROLE lago PASSWORD '${POSTGRES_PASSWORD}' CREATEDB LOGIN;\"" postgres >> ${DATA_DIR}/db.log + +# Launch BE Services +cd ./api +bundle exec rake db:create >> ${DATA_DIR}/db.log +bundle exec rake db:migrate >> ${DATA_DIR}/db.log +bundle exec rails signup:seed_organization >> ${DATA_DIR}/db.log +rm -f ./tmp/pids/server.pid +foreman start diff --git a/scripts/bootstrap.sh b/scripts/bootstrap.sh new file mode 100755 index 000000000..e99f3a553 --- /dev/null +++ b/scripts/bootstrap.sh @@ -0,0 +1,7 @@ +#! /bin/sh + +apt update +apt install -y git curl +curl -sL https://deb.nodesource.com/setup_20.x | sh - +apt update +apt install nodejs npm diff --git a/scripts/pg-init-scripts/bootstrap.sh b/scripts/pg-init-scripts/bootstrap.sh new file mode 100644 index 000000000..e1828b402 --- /dev/null +++ b/scripts/pg-init-scripts/bootstrap.sh @@ -0,0 +1,7 @@ +#!/bin/sh + +apt update +apt install -y git curl +curl -sL https://deb.nodesource.com/setup_20.x | sh +apt update +apt install build-essential nodejs npm