diff --git a/Dockerfile b/Dockerfile index 2cb2c83e796..da81adc1624 100644 --- a/Dockerfile +++ b/Dockerfile @@ -9,7 +9,7 @@ WORKDIR /rails # Install base packages RUN apt-get update -qq \ - && apt-get install --no-install-recommends -y curl libvips postgresql-client libyaml-0-2 procps \ + && apt-get install --no-install-recommends -y curl libvips postgresql-client libyaml-0-2 procps\ && rm -rf /var/lib/apt/lists /var/cache/apt/archives # Set production environment @@ -58,6 +58,9 @@ COPY --chown=rails:rails --from=build /rails /rails # Entrypoint prepares the database. ENTRYPOINT ["/rails/bin/docker-entrypoint"] +HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \ + CMD curl --fail http://localhost:3000/healthcheck || exit 1 + # Start the server by default, this can be overwritten at runtime EXPOSE 3000 CMD ["./bin/rails", "server"] diff --git a/app/controllers/health_controller.rb b/app/controllers/health_controller.rb new file mode 100644 index 00000000000..689d08e19d3 --- /dev/null +++ b/app/controllers/health_controller.rb @@ -0,0 +1,52 @@ +class HealthController < ApplicationController + skip_before_action :verify_authenticity_token + skip_before_action :authenticate_user! + skip_before_action :set_request_details + skip_before_action :set_sentry_user + skip_before_action :verify_self_host_config + + # Silence logging for liveliness endpoint + around_action :silence_logger, only: :liveliness + + def healthcheck + checks = { + database: check_database, + redis: check_redis + } + + all_healthy = checks.values.all? + + status = all_healthy ? :ok : :service_unavailable + render json: checks, status: status + end + + def liveliness + head :ok + end + + private + + def check_database + ActiveRecord::Base.connection.execute("SELECT 1") + true + rescue StandardError => e + Rails.logger.error("Database health check failed: #{e.message}") + false + end + + def check_redis + Sidekiq.redis(&:ping) + true + rescue StandardError => e + Rails.logger.error("Redis health check failed: #{e.message}") + false + end + + def silence_logger + old_level = Rails.logger.level + Rails.logger.level = Logger::UNKNOWN + yield + ensure + Rails.logger.level = old_level + end +end diff --git a/compose.example.yml b/compose.example.yml index 01a97368b0d..35fbd9736ff 100644 --- a/compose.example.yml +++ b/compose.example.yml @@ -52,6 +52,13 @@ services: ports: - 3000:3000 restart: unless-stopped + healthcheck: + test: [ "CMD-SHELL", "curl -f http://0.0.0.0:3000 || exit 1" ] + start_period: 10s + start_interval: 10s + interval: 10s + timeout: 5s + retries: 3 environment: <<: *rails_env depends_on: @@ -73,6 +80,12 @@ services: condition: service_healthy redis: condition: service_healthy + healthcheck: + test: [ "CMD-SHELL", "pgrep -f sidekiq || exit 1" ] + interval: 30s + timeout: 5s + retries: 3 + start_period: 10s environment: <<: *rails_env networks: diff --git a/config/routes.rb b/config/routes.rb index 2812c99977f..f87ef4e7f60 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -333,6 +333,10 @@ # Can be used by load balancers and uptime monitors to verify that the app is live. get "up" => "rails/health#show", as: :rails_health_check + # Health check endpoints + get "healthcheck", to: "health#healthcheck" + get "liveliness", to: "health#liveliness" + # Render dynamic PWA files from app/views/pwa/* get "service-worker" => "rails/pwa#service_worker", as: :pwa_service_worker get "manifest" => "rails/pwa#manifest", as: :pwa_manifest