diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 00000000..08af4e8f --- /dev/null +++ b/.dockerignore @@ -0,0 +1,30 @@ +.git/ +log/* +!log/.keep +app/assets/builds/* +!app/assets/builds/.keep +node_modules/ +public/assets/ +public/packs/ +public/packs-test/ +public/packs-dev/ +public/vite-dev/ +public/vite-test/ +public/vite/ +storage/* +!storage/.keep +tmp/* +!tmp/.keep + +.bundle +.byebug_history +.dockerignore +.env* +!.env.example +config/master.key +docker-compose.override.yml +yarn-error.log +yarn-debug.log* +.yarn-integrity +docker +spec diff --git a/.github/workflows/build-and-push-on-pr.yml b/.github/workflows/build-and-push-on-pr.yml new file mode 100644 index 00000000..10b0848f --- /dev/null +++ b/.github/workflows/build-and-push-on-pr.yml @@ -0,0 +1,48 @@ +name: Create and publish a Docker image + +on: + pull_request: + branches: + - main + +# Defines two custom environment variables for the workflow. These are used for the Container registry domain, and a name for the Docker image that this workflow builds. +env: + REGISTRY: ghcr.io + IMAGE_NAME: ${{ github.repository }} + +# There is a single job in this workflow. It's configured to run on the latest available version of Ubuntu. +jobs: + build-and-push-image: + runs-on: ubuntu-latest + # Sets the permissions granted to the `GITHUB_TOKEN` for the actions in this job. + permissions: + contents: read + packages: write + # + steps: + - name: Checkout repository + uses: actions/checkout@v4 + # Uses the `docker/login-action` action to log in to the Container registry registry using the account and password that will publish the packages. Once published, the packages are scoped to the account defined here. + - name: Log in to the Container registry + uses: docker/login-action@65b78e6e13532edd9afa3aa52ac7964289d1a9c1 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + # This step uses [docker/metadata-action](https://github.com/docker/metadata-action#about) to extract tags and labels that will be applied to the specified image. The `id` "meta" allows the output of this step to be referenced in a subsequent step. The `images` value provides the base name for the tags and labels. + - name: Extract metadata (tags, labels) for Docker + id: meta + uses: docker/metadata-action@9ec57ed1fcdbf14dcef7dfbe97b2010124a938b7 + with: + images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} + # This step uses the `docker/build-push-action` action to build the image, based on your repository's `Dockerfile`. If the build succeeds, it pushes the image to GitHub Packages. + # It uses the `context` parameter to define the build's context as the set of files located in the specified path. For more information, see "[Usage](https://github.com/docker/build-push-action#usage)" in the README of the `docker/build-push-action` repository. + # It uses the `tags` and `labels` parameters to tag and label the image with the output from the "meta" step. + - name: Build and push Docker image + uses: docker/build-push-action@f2a1d5e99d037542a71f64918e516c093c6f3fc4 + with: + context: . + push: true + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + file: docker/Dockerfile diff --git a/bin/docker-entrypoint-web b/bin/docker-entrypoint-web new file mode 100755 index 00000000..ae406368 --- /dev/null +++ b/bin/docker-entrypoint-web @@ -0,0 +1,24 @@ +#!/usr/bin/env bash + +set -e + +# Always keep this here as it ensures your latest built assets make their way +# into your volume persisted public directory. +cp -r /public /app + +# Sprockets will use the first sprockets file it finds not the latest one. We +# need to delete all of the old sprockets files except for the one that was +# last built into the image. That's what the code below does. + +# shellcheck disable=SC2125 +manifest_files=/app/public/assets/.sprockets-manifest-*.json + +if compgen -G "${manifest_files}" > /dev/null 2>&1; then + # shellcheck disable=SC2086,SC2061 + find \ + ${manifest_files} \ + -type f ! -name "$(basename /public/assets/.sprockets-manifest-*.json)" \ + -delete +fi + +exec "$@" diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 00000000..abd9b69f --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,127 @@ +x-app: &default-app + build: + context: "." + dockerfile: "./docker/Dockerfile" + target: "app" + args: + - "UID=${UID:-1000}" + - "GID=${GID:-1000}" + - "RAILS_ENV=${RAILS_ENV:-production}" + - "NODE_ENV=${NODE_ENV:-production}" + depends_on: + postgres: + condition: "service_started" + required: false + redis: + condition: "service_started" + required: false + env_file: + - "./docker/.env" + restart: "${DOCKER_RESTART_POLICY:-unless-stopped}" + stop_grace_period: "3s" + tty: true + volumes: + - "${DOCKER_WEB_VOLUME:-./public:/app/public}" + +x-assets: &default-assets + build: + context: "." + dockerfile: "./docker/Dockerfile" + target: "assets" + args: + - "UID=${UID:-1000}" + - "GID=${GID:-1000}" + - "RAILS_ENV=${RAILS_ENV:-production}" + - "NODE_ENV=${NODE_ENV:-production}" + env_file: + - "./docker/.env" + profiles: ["assets"] + restart: "${DOCKER_RESTART_POLICY:-unless-stopped}" + stop_grace_period: "3s" + tty: true + volumes: + - ".:/app" + +services: + postgres: + deploy: + resources: + limits: + cpus: "${DOCKER_POSTGRES_CPUS:-0}" + memory: "${DOCKER_POSTGRES_MEMORY:-0}" + environment: + POSTGRES_USER: "${POSTGRES_USER}" + POSTGRES_PASSWORD: "${POSTGRES_PASSWORD}" + # POSTGRES_DB: "${POSTGRES_DB}" + image: "postgres:16.1-bookworm" + profiles: ["dev"] + restart: "${DOCKER_RESTART_POLICY:-unless-stopped}" + stop_grace_period: "3s" + volumes: + - "postgres:/var/lib/postgresql/data" + + redis: + deploy: + resources: + limits: + cpus: "${DOCKER_REDIS_CPUS:-0}" + memory: "${DOCKER_REDIS_MEMORY:-0}" + image: "redis:7.2.3-bookworm" + profiles: ["dev"] + restart: "${DOCKER_RESTART_POLICY:-unless-stopped}" + stop_grace_period: "3s" + volumes: + - "redis:/data" + solr: + image: "solr:8.4" + profiles: ["dev"] + volumes: + - "./solr/conf:/core/conf" + command: "solr-precreate core /core" + + web: + <<: *default-app + deploy: + resources: + limits: + cpus: "${DOCKER_WEB_CPUS:-0}" + memory: "${DOCKER_WEB_MEMORY:-0}" + depends_on: + postgres: + condition: "service_started" + required: false + redis: + condition: "service_started" + required: false + migration: + condition: "service_started" + required: false + healthcheck: + test: "${DOCKER_WEB_HEALTHCHECK_TEST:-curl localhost:8000/up}" + interval: "60s" + timeout: "3s" + start_period: "5s" + retries: 3 + ports: + - "${DOCKER_WEB_PORT_FORWARD:-127.0.0.1:8000}:${PORT:-8000}" + profiles: ["web"] + + worker: + <<: *default-app + command: "bundle exec sidekiq -C config/sidekiq.yml" + entrypoint: [] + deploy: + resources: + limits: + cpus: "${DOCKER_WORKER_CPUS:-0}" + memory: "${DOCKER_WORKER_MEMORY:-0}" + profiles: ["worker"] + migration: + <<: *default-app + entrypoint: [] + command: "bash -c \"bundle exec rake db:create && bundle exec rake db:migrate\"" + restart: no + +volumes: + postgres: {} + redis: {} diff --git a/docker/Dockerfile b/docker/Dockerfile new file mode 100644 index 00000000..e4f80dad --- /dev/null +++ b/docker/Dockerfile @@ -0,0 +1,103 @@ +FROM ruby:3.1.0-slim-bullseye AS assets + +WORKDIR /app + +ARG UID=1000 +ARG GID=1000 +ENV NVM_DIR /usr/local/nvm +ARG NODE_VERSION=18.18.0 +ENV NODE_VERSION="${NODE_VERSION}" + +RUN bash -c "set -o pipefail && apt-get update \ + && apt-get install -y --no-install-recommends build-essential curl git libpq-dev python3-pip \ + && mkdir -p /usr/local/nvm \ + && curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.7/install.sh | bash \ + && source $NVM_DIR/nvm.sh \ + && nvm install $NODE_VERSION \ + && nvm alias default $NODE_VERSION \ + && nvm use default \ + && npm install --global yarn \ + && yarn global add node-gyp \ + && rm -rf /var/lib/apt/lists/* /usr/share/doc /usr/share/man \ + && apt-get clean \ + && groupadd -g \"${GID}\" ruby \ + && useradd --create-home --no-log-init -u \"${UID}\" -g \"${GID}\" ruby \ + && mkdir /node_modules && chown ruby:ruby -R /node_modules /app" +ENV NODE_PATH $NVM_DIR/v$NODE_VERSION/lib/node_modules +ENV PATH $NVM_DIR/versions/node/v$NODE_VERSION/bin:$PATH + +USER ruby + +COPY --chown=ruby:ruby Gemfile* ./ +RUN bundle config --global frozen 1 && bundle config set --local without 'development test' && bundle install + +COPY --chown=ruby:ruby package.json *yarn* ./ +RUN yarn install + +ARG RAILS_ENV="production" +ARG NODE_ENV="production" +ENV RAILS_ENV="${RAILS_ENV}" \ + NODE_ENV="${NODE_ENV}" \ + PATH="${PATH}:/home/ruby/.local/bin:/node_modules/.bin" \ + USER="ruby" + +COPY --chown=ruby:ruby . . + +RUN if [ "${RAILS_ENV}" != "development" ]; then \ + SECRET_KEY_BASE=1 rails assets:precompile; fi + +CMD ["bash"] + +############################################################################### + +FROM ruby:3.1.0-slim-bullseye AS app + +WORKDIR /app + +ARG UID=1000 +ARG GID=1000 +ENV NVM_DIR /usr/local/nvm +ARG NODE_VERSION=18.18.0 +ENV NODE_VERSION="${NODE_VERSION}" + +RUN apt-get update \ + && apt-get install -y --no-install-recommends build-essential python3-pip curl libpq-dev \ + && rm -rf /var/lib/apt/lists/* /usr/share/doc /usr/share/man \ + && apt-get clean \ + && groupadd -g "${GID}" ruby \ + && useradd --create-home --no-log-init -u "${UID}" -g "${GID}" ruby \ + && chown ruby:ruby -R /app +ENV NODE_PATH $NVM_DIR/v$NODE_VERSION/lib/node_modules +ENV PATH $NVM_DIR/versions/node/v$NODE_VERSION/bin:$PATH +# We have to install NodeJS in the app until we don't have execjs for the +# bootstrap gem. +RUN bash -c "set -o pipefail && \ + mkdir -p /usr/local/nvm \ + && curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.7/install.sh | bash \ + && source $NVM_DIR/nvm.sh \ + && nvm install $NODE_VERSION \ + && nvm alias default $NODE_VERSION \ + && nvm use default" + +USER ruby + +COPY --chown=ruby:ruby bin/ ./bin +RUN chmod 0755 bin/* + +ARG RAILS_ENV="production" +ENV RAILS_ENV="${RAILS_ENV}" \ + PATH="${PATH}:/home/ruby/.local/bin" \ + USER="ruby" + +COPY --chown=ruby:ruby --from=assets /usr/local/bundle /usr/local/bundle +COPY --chown=ruby:ruby --from=assets /app/public /public +COPY --chown=ruby:ruby . . +RUN bundle config --global frozen 1 +RUN bundle config set --local without 'development test' +RUN bundle install + +ENTRYPOINT ["/app/bin/docker-entrypoint-web"] + +EXPOSE 8000 + +CMD ["rails", "s", "-p", "8000"] diff --git a/docker/job.hcl b/docker/job.hcl new file mode 100644 index 00000000..2ed8db1e --- /dev/null +++ b/docker/job.hcl @@ -0,0 +1,192 @@ +job "pulfalight" { + region = "global" + datacenters = ["dc1"] + type = "service" + group "rdbms" { + count = 1 + network { + port "postgres" { to = 5432 } + } + service { + port = "postgres" + check { + name = "postgres_probe" + type = "tcp" + interval = "10s" + timeout = "1s" + } + } + task "postgres" { + driver = "podman" + config { + image = "docker.io/library/postgres:15.5-bullseye" + ports = ["postgres"] + } + template { + destination = "${NOMAD_SECRETS_DIR}/env.vars" + env = true + change_mode = "restart" + data = <