diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json new file mode 100644 index 0000000000..fa7e364ea6 --- /dev/null +++ b/.devcontainer/devcontainer.json @@ -0,0 +1,24 @@ +{ + "name": "DevDocs Dev Container", + "build": { + "context": "../", + "dockerfile": "../Dockerfile", + "target": "devdocs-dev" + }, + "customizations": { + "vscode": { + "extensions": [ + "streetsidesoftware.code-spell-checker", + "mhutchie.git-graph", + "github.vscode-github-actions", + "ms-azuretools.vscode-docker", + "EditorConfig.EditorConfig" + ] + } + }, + "mounts": [ + {"source":"/var/run/docker.sock","target":"/var/run/docker.sock","type": "bind"}, + {"source": "devdocs-downloaded","target": "/devdocs/public/docs","type": "volume"} + ], + "appPort": 9292 +} diff --git a/.dockerignore b/.dockerignore index 64b8f28ccf..0a0fd556eb 100644 --- a/.dockerignore +++ b/.dockerignore @@ -5,3 +5,9 @@ Dockerfile* .dockerignore .travis.yml *.md +public/docs/* +public/assets/* +log/* +tmp/* +test/* +.bash_history \ No newline at end of file diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000000..2125666142 --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +* text=auto \ No newline at end of file diff --git a/.gitignore b/.gitignore index bbf749a452..58e6ee5950 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,7 @@ .DS_Store .bundle +.bash_history +.env log tmp public/assets diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000000..80b1c05bb7 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,6 @@ +{ + "cSpell.words": [ + "devdocs", + "rackup" + ] +} diff --git a/Dockerfile b/Dockerfile index 848927d0b3..96a72f6689 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,24 +1,56 @@ -FROM ruby:3.3.2 +# +# Base layer that both dev and runtime inherit from. +# +FROM ruby:3.3.2-alpine as devdocs-base + ENV LANG=C.UTF-8 -ENV ENABLE_SERVICE_WORKER=true +ARG USERNAME=devdocs +ARG USER_ID=1000 +ARG GROUP_ID=1000 WORKDIR /devdocs +EXPOSE 9292 -RUN apt-get update && \ - apt-get -y install git nodejs libcurl4 && \ - gem install bundler && \ - rm -rf /var/lib/apt/lists/* +COPY Gemfile Gemfile.lock Rakefile Thorfile /devdocs/ -COPY Gemfile Gemfile.lock Rakefile /devdocs/ +RUN apk --update add nodejs build-base libstdc++ gzip git zlib-dev libcurl oxipng && \ + rm -rf /var/cache/apk/* /usr/lib/node_modules -RUN bundle install --system && \ - rm -rf ~/.gem /root/.bundle/cache /usr/local/bundle/cache +RUN gem install bundler && \ + bundle config set path.system true && \ + bundle config set without test && \ + bundle install && \ + rm -rf ~/.gem /root/.bundle/cache /usr/local/bundle/cache -COPY . /devdocs +RUN addgroup -g $GROUP_ID $USERNAME && \ + adduser -u $USER_ID -G $USERNAME -D -h /devdocs $USERNAME && \ + chown -R $USERNAME:$USERNAME /devdocs -RUN thor docs:download --all && \ - thor assets:compile && \ - rm -rf /tmp +# +# Development Image +# +FROM devdocs-base as devdocs-dev +RUN bundle config unset without && \ + bundle install && \ + apk add --update bash curl && \ + curl -LO https://download.docker.com/linux/static/stable/x86_64/docker-26.1.4.tgz && \ + tar -xzf docker-26.1.4.tgz && \ + mv docker/docker /usr/bin && \ + rm -rf docker docker-26.1.4.tgz && \ + rm -rf ~/.gem /root/.bundle/cache /usr/local/bundle/cache -EXPOSE 9292 -CMD rackup -o 0.0.0.0 +VOLUME [ "/devdocs", "/devdocs/public/docs", "/devdocs/public/assets" ] +CMD bash + +# +# Runtime Image +# +FROM devdocs-base as devdocs +ENV ENABLE_SERVICE_WORKER=true +COPY . /devdocs/ +RUN apk del gzip build-base git zlib-dev && \ + chown -R $USERNAME:$USERNAME /devdocs && \ + rm -rf /tmp +VOLUME [ "/devdocs/public/docs", "/devdocs/public/assets" ] +USER $USERNAME +CMD rackup --host 0.0.0.0 -E production diff --git a/Dockerfile-alpine b/Dockerfile-alpine deleted file mode 100644 index 0e70a00da3..0000000000 --- a/Dockerfile-alpine +++ /dev/null @@ -1,20 +0,0 @@ -FROM ruby:3.3.2-alpine - -ENV LANG=C.UTF-8 -ENV ENABLE_SERVICE_WORKER=true - -WORKDIR /devdocs - -COPY . /devdocs - -RUN apk --update add nodejs build-base libstdc++ gzip git zlib-dev libcurl && \ - gem install bundler && \ - bundle install --system --without test && \ - thor docs:download --all && \ - thor assets:compile && \ - apk del gzip build-base git zlib-dev && \ - rm -rf /var/cache/apk/* /tmp ~/.gem /root/.bundle/cache \ - /usr/local/bundle/cache /usr/lib/node_modules - -EXPOSE 9292 -CMD rackup -o 0.0.0.0 diff --git a/README.md b/README.md index a54f4992ee..87927d59fc 100644 --- a/README.md +++ b/README.md @@ -38,15 +38,54 @@ The `thor docs:download` command is used to download pre-generated documentation **Note:** there is currently no update mechanism other than `git pull origin main` to update the code and `thor docs:download --installed` to download the latest version of the docs. To stay informed about new releases, be sure to [watch](https://github.com/freeCodeCamp/devdocs/subscription) this repository. -Alternatively, DevDocs may be started as a Docker container: +## Deploying via Docker +The DevDocs server may also be deployed as a Docker container: ```sh -# First, build the image -git clone https://github.com/freeCodeCamp/devdocs.git && cd devdocs -docker build -t thibaut/devdocs . +# First, pull the image +docker pull devdocs/devdocs + +# Next, download documentation that you want, this example downloads the default set of docs, and the ruby docs. +# Use: --default to download the default set of docs +# Use: --all to download ALL available docs (WARNING: Will take a long time!) +docker run --rm \ + -v devdocs-docs:/devdocs/public/docs \ + -v devdocs-assets:/devdocs/public/assets \ + devdocs/devdocs thor docs:download --default ruby + +# Now compile the assets. This must be done after you download all the documentation you want. +docker run --rm \ + -v devdocs-docs:/devdocs/public/docs \ + -v devdocs-assets:/devdocs/public/assets \ + devdocs/devdocs thor assets:compile + +# Start the DevDocs container (accessible at http://localhost:9292) +docker run \ + -v devdocs-docs:/devdocs/public/docs \ + -v devdocs-assets:/devdocs/public/assets \ + -e DEVDOCS_DISABLE_SSL \ + -e DEVDOCS_DOCS_ORIGIN=localhost:9292 \ + -e DEVDOCS_HOST=localhost:9292 \ + -p 9292:9292 \ + devdocs/devdocs +``` + +The `devdocs-docs` and `devdocs-assets` volumes contain the downloaded documentation and static site data used by the server. + +There are multiple environment variables that you can set to configure the DevDocs server. + +These can be useful when deploying DevDocs behind a reverse proxy or on your own offline network. + +| Environment Variable | Default Value | Description | +|---------------------:|:---------------------|:------------------------------------------------------------------------------------------------| +|`DEVDOCS_DISABLE_SSL` |Not defined | Define this variable to disable HTTPS redirect. | +|`DEVDOCS_HOST` |`devdocs.io` | Hostname that is serving the DevDocs application. | +|`DEVDOCS_DOCS_ORIGIN` |`documents.devdocs.io`| Hostname that is serving the DevDocs documentation pages. | +|`DEVDOCS_DISABLE_HSTS`|Not defined | Define this variable to disable HSTS. If `DEVDOCS_DISABLE_SSL` is defined then this is implied. | -# Finally, start a DevDocs container (access http://localhost:9292) -docker run --name devdocs -d -p 9292:9292 thibaut/devdocs +To build the image fresh, use the below command: +```bash +docker build . -t devdocs/devdocs --target devdocs ``` ## Vision diff --git a/assets/javascripts/app/config.js.erb b/assets/javascripts/app/config.js.erb index 56ce26522f..f27efa4eb6 100644 --- a/assets/javascripts/app/config.js.erb +++ b/assets/javascripts/app/config.js.erb @@ -1,13 +1,13 @@ app.config = { db_filename: 'db.json', default_docs: <%= App.default_docs.to_json %>, - docs_origin: '<%= App.docs_origin %>', + docs_origin: document.getElementById("docs-origin-meta").content, env: '<%= App.environment %>', history_cache_size: 10, index_filename: 'index.json', index_path: '/<%= App.docs_prefix %>', max_results: 50, - production_host: 'devdocs.io', + production_host: document.getElementById("production-host-meta").content, search_param: 'q', sentry_dsn: '<%= App.sentry_dsn %>', version: <%= Time.now.to_i %>, diff --git a/assets/javascripts/models/doc.js b/assets/javascripts/models/doc.js index 53d573cde2..707ace6328 100644 --- a/assets/javascripts/models/doc.js +++ b/assets/javascripts/models/doc.js @@ -43,11 +43,11 @@ app.models.Doc = class Doc extends app.Model { } fileUrl(path) { - return `${app.config.docs_origin}${this.fullPath(path)}?${this.mtime}`; + return `//${app.config.docs_origin}${this.fullPath(path)}?${this.mtime}`; } dbUrl() { - return `${app.config.docs_origin}/${this.slug}/${app.config.db_filename}?${this.mtime}`; + return `//${app.config.docs_origin}/${this.slug}/${app.config.db_filename}?${this.mtime}`; } indexUrl() { diff --git a/lib/app.rb b/lib/app.rb index e23b241c06..55c0dda062 100644 --- a/lib/app.rb +++ b/lib/app.rb @@ -12,7 +12,10 @@ class App < Sinatra::Application Rack::Mime::MIME_TYPES['.webapp'] = 'application/x-web-app-manifest+json' configure do - use Rack::SslEnforcer, only_environments: ['production', 'test'], hsts: true, force_secure_cookies: false + + unless ENV.has_key?('DEVDOCS_DISABLE_SSL') + use Rack::SslEnforcer, only_environments: ['production', 'test'], hsts: !ENV.has_key?('DEVDOCS_DISABLE_HSTS'), force_secure_cookies: false + end set :sentry_dsn, ENV['SENTRY_DSN'] set :protection, except: [:frame_options, :xss_header] @@ -26,6 +29,7 @@ class App < Sinatra::Application set :assets_compile, %w(*.png docs.js docs.json application.js application.css application-dark.css) require 'yajl/json_gem' + set :docs_host, ENV.fetch("DEVDOCS_HOST", "devdocs.io") set :docs_prefix, 'docs' set :docs_origin, File.join('', docs_prefix) set :docs_path, File.join(public_folder, docs_prefix) @@ -72,7 +76,7 @@ class App < Sinatra::Application configure :production do set :static, false - set :docs_origin, '//documents.devdocs.io' + set :docs_origin, ENV.fetch("DEVDOCS_DOCS_ORIGIN", "documents.devdocs.io") set :csp, "default-src 'self' *; script-src 'self' 'nonce-devdocs' https://www.google-analytics.com https://secure.gaug.es https://*.jquery.com; font-src 'none'; style-src 'self' 'unsafe-inline' *; img-src 'self' * data:;" use Rack::ConditionalGet @@ -125,8 +129,8 @@ def self.parse_news end configure :production do - set :docs, parse_docs - set :news, parse_news + set :docs, -> { parse_docs } + set :news, -> { parse_news } end helpers do @@ -421,8 +425,10 @@ def modern_browser?(browser) redirect "/#{doc}#{type}/#{query_string_for_redirection}" elsif rest.length > 1 && rest.end_with?('/') redirect "/#{doc}#{type}#{rest[0...-1]}#{query_string_for_redirection}" - elsif user_has_docs?(doc) && supports_js_redirection? + elsif !request.path.end_with?(".html") && user_has_docs?(doc) && supports_js_redirection? redirect_via_js(request.path) + elsif settings.docs_host == settings.docs_origin && File.exist?(File.join(settings.public_folder, "docs", request.path.gsub("..",""))) + send_file File.join(settings.public_folder, "docs", request.path.gsub("..","")), status: status else response.headers['Content-Security-Policy'] = settings.csp if settings.csp erb :other diff --git a/views/index.erb b/views/index.erb index 428dc705a9..d9ccc0515c 100644 --- a/views/index.erb +++ b/views/index.erb @@ -21,6 +21,8 @@ + +