├── .gitignore ├── Dockerfile.template ├── README.md ├── bin ├── build ├── exec ├── push ├── start └── vars ├── docker-compose.yml.template └── docker-entrypoint.sh /.gitignore: -------------------------------------------------------------------------------- 1 | Dockerfile 2 | docker-compose.yml 3 | -------------------------------------------------------------------------------- /Dockerfile.template: -------------------------------------------------------------------------------- 1 | # This is using Docker's official Ruby base image, for 2 | # arm64v8, which is Apple Silicon. This image is the one 3 | # used for Rails by default and is maintained by Docker. 4 | # 5 | # You can see how the image is made by going here: 6 | # 7 | # https://github.com/docker-library/ruby/blob/8a6d8c6532c1f918bae81d83b2fedc9b5bd7aada/3.2/bullseye/Dockerfile 8 | # 9 | # To determine the OS, you must navigate backwards using the FROM directive from 10 | # that Dockerfile. You will find your way to Debian's repo here: 11 | # 12 | # https://github.com/debuerreotype/docker-debian-artifacts/blob/2776d8c4859a7e6e275ee8b5dd144488d7387c24/bullseye/Dockerfile 13 | # 14 | # Thus, this is a Debian-based build. 15 | FROM arm64v8/ruby:3.2 16 | 17 | # Before we start, you'll note there are a lot of RUN directives. Each of these creates a layer that is cached by 18 | # Docker. Thus, if you change the second RUN directive and rebuild the image, the first RUN directive doesn't have 19 | # to be re-executed - the cached layer is used and the second directive is executed from there. 20 | # 21 | # Each RUN directive in here is chosen to allow the image to evolve over time while minimizing the amount of 22 | # re-building that has to be done. 23 | # 24 | # Thus, the first few things set up stuff that is unlikely to change frequently, such as OS-level tools and Node. 25 | # Later directives do stuff like Chrome, which will change a lot and is also unstable. 26 | 27 | # This tells our apt-get calls to not ask for input, but does not releive us of the 28 | # great responsibility of using -y to all commands to indicate that yes, our invocation 29 | # of the command to install a package means we do, in fact, want that package installed. 30 | ENV DEBIAN_FRONTEND noninteractive 31 | 32 | # This is copied from the Rails devcontainer and installs base packages we will need for other things we'll 33 | # install. 34 | RUN apt-get update -qq && \ 35 | apt-get install -y \ 36 | build-essential \ 37 | curl \ 38 | git \ 39 | libvips \ 40 | procps \ 41 | rsync \ 42 | wget 43 | 44 | # This installs the latest supported verison of Node. It should be 45 | # an even numbered version unless you are doing Node development. 46 | RUN curl -sL https://deb.nodesource.com/setup_18.x | bash - && \ 47 | apt-get install -y nodejs && \ 48 | npm install -g yarn 49 | 50 | # Now install the Postgres client, which is needed for the Postgres gem. We don't need 51 | # the entire Postgres server in here. 52 | # These instructions are from https://www.postgresql.org/download/linux/ubuntu/ 53 | # This is needed because this version of Debian does not have Postgres 15 and we want to be on 54 | # the latest version. 55 | RUN sh -c 'echo "deb http://apt.postgresql.org/pub/repos/apt $(lsb_release -cs)-pgdg main" > /etc/apt/sources.list.d/pgdg.list' && \ 56 | wget --quiet -O - https://www.postgresql.org/media/keys/ACCC4CF8.asc | apt-key add - && \ 57 | apt-get update && \ 58 | apt-get -y install postgresql-client-15 59 | 60 | # Next, we install chromium and chromium-driver to facilitate Rails system tests. 61 | # There is no Chrome for ARM linux distributions, so we have to use Chromium. 62 | # Note that although Apple Silicon is capable of emulating AMD instructions, Chrome 63 | # requires system calls that are not emulated. It is unclear if Apple, Docker, or Google 64 | # are the ones who could fix this, but none of them are at the moment. 65 | RUN apt-get -y install chromium chromium-driver 66 | 67 | # Now, we set up RubyGems to avoid building documentation. We don't need the docs 68 | # inside this image and it slows down gem installation to have them. 69 | # We also install the latest bundler as well as Rails. Lastly, to help debug stuff inside 70 | # the container, I need vi mode on the command line and vim to be installed. These can be 71 | # omitted and aren't needed by the Rails toolchain. 72 | RUN echo "gem: --no-document" >> ~/.gemrc && \ 73 | gem install bundler && \ 74 | gem install rails --version ">= 7.0.0" && \ 75 | echo "set -o vi" >> ~/.bashrc && \ 76 | apt-get -y install vim 77 | 78 | # Allows rails server to be accessible outside this docker container 79 | ENV BINDING="0.0.0.0" 80 | 81 | # This entrypoint produces a nice help message and waits around for you to do 82 | # something with the container. 83 | COPY ./docker-entrypoint.sh /root 84 | 85 | # This says to expose the given port to the outside world. 86 | EXPOSE %EXPOSE% 87 | 88 | # Although WORKDIR specifies a base directory where COPY, ADD, RUN, etc. commands run from, 89 | # we use it here as a default directory where docker run AKA bin/exec commands will 90 | # be run from. Essentially if you bin/exec bash, this is the directory you will end up in. 91 | # It is also where docker-compose.yml will map your local directory. 92 | WORKDIR %WORKDIR% 93 | # vim: ft=Dockerfile 94 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # SUPERCEDED BY https://github.com/sustainable-rails/sustainable-rails-dev 2 | 3 | ---- 4 | 5 | # Dev Environment for Sustainable Web Development with Ruby on Rails 6 | 7 | This is a basis for creating a Ruby on Rails development environment using Docker. It is based on the development environment used for my book [Sustainable Web Development with Ruby on Rails](https://sustainable-rails.com) and can be used and customized to meet any needs for doing local development. 8 | 9 | ## What problem does this solve? 10 | 11 | * Setting up dependencies for developing Rails apps is not easy, especially as operating systems change. 12 | * Although Docker can solve this, it is difficult to use and difficult to configure. 13 | 14 | ## How does it solve it? 15 | 16 | This provides a template for a `Dockerfile` in which your Rails app can run. It also provides a template for a 17 | `docker-compose.yml` file you can use to run dependent services like Postgres or Redis. It then provides several 18 | shell scripts that wrap the functionality you will need to do work. 19 | 20 | It is currently set up to produce a Debian-based ARM64 image, because this is what is needed to do work on Apple 21 | Silicon. Although Apple Silicon can emulate Intel and AMD-based images, the emulation is incomplete and, in 22 | particular, Chrome does not work. See *Customizing* below if you don't want to use ARM64 images. 23 | 24 | ## How to Use 25 | 26 | 1. Make sure Docker is installed 27 | 1. Clone this repo 28 | 1. Open `bin/vars` and edit the values in there as instructed. You should only need to change `ACCOUNT`, `REPO`, 29 | and `TAG`. 30 | 1. Run `bin/build` 31 | 1. Run `bin/start` 32 | 1. In another terminal, run `bin/exec bash`. You will now be "logged in" to the Docker container where your app 33 | can run. This Docker container has: 34 | * Ruby & Bundler 35 | * Rails 36 | * Node 37 | * Chromium and chromedriver 38 | 39 | ## Customizing 40 | 41 | This repo is intended to be the basis for *your* dev environment, so my recommendation is to create a new repo 42 | where you manage your dev-environment and seed it with these files. 43 | 44 | There are four main points where you can customize things: 45 | 46 | * `bin/vars` - this allows limited control over things like the image name and ports. 47 | * `docker-compose.yml.template` - If you need a different database, don't need redis, need elastic search, etc, you 48 | can add services here as needed. This version includes Postgres and Redis, so you can use those as examples for 49 | what to do. Most common pieces of infrastructure can be run in Docker, so you should be able to get what you need. 50 | `bin/build` will currently replace the following variables when it's run: 51 | - `%TAG%` - The value of `TAG` from `bin/vars` 52 | - `%REPO%` - The value of `REPO` from `bin/vars` 53 | - `%ACCOUNT%` - The value of `ACCOUNT` from `bin/vars` 54 | - `%EXPOSE%` - The value of `EXPOSE` from `bin/vars` 55 | - `%WORKDIR%` - The value of `WORKDIR` from `bin/vars` 56 | - `%LOCAL_PORT%` - The value of `LOCAL_PORT` from `bin/vars` 57 | - `%VOLUME_SOURCE%` - The full path to the current working directory. 58 | * `Dockerfile.template` - this is the basis for the `Dockerfile` that is generated by `bin/build`. You can add whatever you like in here, customizing anything as needed. You may wish to remove the many comments in there that explain how it's set up. You also may wish to modify the architecture. Basically, this is a vanilla `Dockerfile` that you can do what you want to, but it has two variables that are replaced by `bin/build`: 59 | - `%EXPOSE%` - The value of `EXPOSE` from `bin/vars` 60 | - `%WORKDIR%` - The value of `WORKDIR` from `bin/vars` 61 | * The scripts in `bin/` - You can modify and enhance these scripts to provide more customizations as needed. I 62 | strongly recommend maintaing these scripts as no-arg, dependency-free shell scripts, because that will be the 63 | simplest and most reliable way to keep your dev environment consistent and working. Aliases and required 64 | command-line arguments are just annoying. 65 | 66 | ## Where is this being used? 67 | 68 | This was extracted from the toolchain I created for my book, [Sustainable Web Development with Ruby on Rails](https://sustainable-rails.com). All the examples and code in that book were executed many times in this development environment. I have also been using this for the past two years at my startup to manage the development of two Rails apps and a Zendesk sidebar app. 69 | 70 | ## The World's Quickest Docker Tutorial 71 | 72 | Docker is confusing, poorly documented, and poorly designed, so if it makes you feel as stupid as it makes me feel, that is OK. Here is a bit of conceptual grounding to help understand what is going on. 73 | 74 | * A `Dockerfile` is a set of instructions for a computer you would like to run. 75 | * *Building* a `Dockerfile` produces an *Image*. This is a set of bytes on your hard drive and you could consider 76 | this to be a clone of the hard drive of a computer you want to run. 77 | * *Starting* an image produces a *Container*. This is a virtualized computer running the image 78 | * In `docker-compose.yml`, there are services, which describe the containers you want to run in unision. Because a 79 | container is a virtualized computer running an image, each service requires an image that will be run. All the 80 | containers are run on the same network and can see each other. 81 | * When you see the word *Host* that is *your computer*. That is where Docker is running. I cannot think of a more confusing and 82 | unintuitive term but that is what they went with. 83 | 84 | To make another analogy, `Dockerfile` is like source code to a class. An image is the class itself, and a container 85 | is an object you created from that class. `docker-compose.yml` is your program that integrates objects of several 86 | classes together. 87 | 88 | ## Helpful Notes 89 | 90 | Inside the container, you can connect to Postgres like so: 91 | 92 | ``` 93 | > PGPASSWORD=postgres psql --host db --username postgres --port 5432 94 | postgres=# 95 | ``` 96 | 97 | When you run Rails, you need to tell it to bind to `0.0.0.0`, so you can't just do `bin/rails s`. Instead you 98 | must: 99 | ``` 100 | > bin/rails s --binding 0.0.0.0 101 | ``` 102 | 103 | When you do that, your Rails app should be available to your localhost on port 9999 (or whatever value you set in `bin/vars` for `EXPOSE`) 104 | 105 | ## Core Values 106 | 107 | * As few dependencies as possible 108 | * Your computer is not your development environment, it *runs* your development environment 109 | * Useful for working professionals 110 | * Programmers should understand how their development environment works 111 | 112 | ### Non-Values 113 | 114 | * Flexibility 115 | * Production Deployments 116 | * Hiding details about how this works from the user 117 | 118 | 119 | ### Things That Could be Improved 120 | 121 | * A way to QA this on other platforms like Linux or Windows without me having to buy a Linux machine or a Windows 122 | machine. 123 | * Ability to target more than just ARM64 without a lot of customizations 124 | * Probably some Docker best practices I'm not aware of needing to consider 125 | 126 | ## FAQ 127 | 128 | ### Why is this ARM64? 129 | 130 | When running Chrome inside an AMD-based Docker container, Apple Silicon is unable to emulate certain system calls 131 | it needs, thus you cannot run Rails system tests in an AMD-based Docker container running on Apple Silicon. 132 | 133 | ### Why is this using Chromium? 134 | 135 | See the answer above. Chrome is not available for ARM64-based Linux operating systems, however Chromium is. 136 | 137 | ### How can I use AMD-based Images? 138 | 139 | Change this: 140 | 141 | ``` 142 | FROM arm64v8/ruby:3.1 143 | ``` 144 | 145 | to whatever base image you like, such as `amd64/ruby:3.1`. 146 | 147 | If you do that, you don't have to use chromium. You can remove this line: 148 | 149 | ``` 150 | RUN apt-get -y install chromium chromium-driver 151 | ``` 152 | 153 | You will need to replace it with a more convoluted set of invocations that has changed many times since I first 154 | created this repo, so I will not document them here, as they are likely to be out of date. I'm sorry about that, 155 | but Google does not care about making this process easy. 156 | 157 | ### What about that docked rails thing? 158 | 159 | The Rails GitHub org has a repo called [docked](https://github.com/rails/docked) that ostensibly sets up a dev 160 | environment for Rails. It may evolve to be more useful, but here are the problems it has that this repo does not: 161 | 162 | * It will not work on Apple Silicon for reasons mentioned above re: Chrome 163 | * It does not provide a solution for running dependent infrastructure like Postgres which, in my experience, is much 164 | harder to do than getting Rails running. 165 | * It requires setting up shell aliases, which I dislike. 166 | * It uses an odd-numbered version of Node, and it's not a good idea to use that for development or production 167 | unless you are working on Node itself. Odd-numbered versions go end-of-life frequently and become unsupported. It's better and safer to use even-numbered versions. 168 | * It is designed for beginners to programming and Rails, which is great because we need more Rails developers, but 169 | that is a different use-case than a development environment for professional, experienced Rails developers. And 170 | yes, I mean "professional" in the sense of "getting paid to write Rails" and not in whatever stupid way Uncle Bob 171 | means it. 172 | 173 | ### Why is this all generated from templates? 174 | 175 | `docker-compose.yml` and `Dockerfile` share some values, but Docker provides no easy mechanism for that that I could figure out. So, the files are generated. 176 | 177 | ### Why not use Vagrant? 178 | 179 | Docker is a more generally useful skill to have, so I decided to focus on this and not learn Vagrant, which is less 180 | useful. 181 | 182 | ## Contributions 183 | 184 | I would love some! I am not a Docker expert and I only have used this for the way I do Rails. I'd love for this to 185 | be more useful (see above). 186 | 187 | A few things I am not interested in: 188 | 189 | * Adding dependencies. I love Ruby and love writing command line apps in Ruby but Ruby's exsitence is not 190 | reliable, which is why the scripts are in bash. 191 | * Non-Rails web development. I want this to be about Rails 192 | * Deprecated or unsupported versions of things 193 | -------------------------------------------------------------------------------- /bin/build: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | set -e 3 | 4 | . bin/vars 5 | 6 | echo "[ bin/build ] 🚢 Generating Dockerfile" 7 | echo "# This is generated, please edit" > Dockerfile 8 | echo "# Dockerfile.template if you want to" >> Dockerfile 9 | echo "# make changes. Also note that some" >> Dockerfile 10 | echo "# values come from bin/vars, so check that out as well" >> Dockerfile 11 | 12 | cat Dockerfile.template | \ 13 | sed "s:%EXPOSE%:$EXPOSE:" | \ 14 | sed "s:%WORKDIR%:$WORKDIR:" \ 15 | >> Dockerfile 16 | 17 | echo "[ bin/build ] 🚢 Generating docker-compose.yml" 18 | cat docker-compose.yml.template | \ 19 | sed "s:%TAG%:$TAG:g" | \ 20 | sed "s:%REPO%:$REPO:g" | \ 21 | sed "s:%ACCOUNT%:$ACCOUNT:g" | \ 22 | sed "s:%EXPOSE%:$EXPOSE:g" | \ 23 | sed "s:%LOCAL_PORT%:$LOCAL_PORT:g" | \ 24 | sed "s:%VOLUME_SOURCE%:`pwd`:g" | \ 25 | sed "s:%WORKDIR%:$WORKDIR:g" \ 26 | > docker-compose.yml 27 | 28 | echo "[ bin/build ] 🚢 Building image" 29 | docker build -t $ACCOUNT/$REPO:$TAG ./ 30 | 31 | echo "🌈 Your Docker image has been built tagged '${ACCOUNT}/${REPO}:${TAG}'" 32 | # vim: ft=bash 33 | -------------------------------------------------------------------------------- /bin/exec: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . bin/vars 3 | 4 | set -e 5 | 6 | CMD=$* 7 | 8 | if [ -z "$CMD" ]; then 9 | echo "😐 You should specify a command to run e.g. $0 ls -l" 10 | exit 1 11 | fi 12 | if [ "$CMD" = "help" ] || [ "$CMD" = "--help" ] || [ "$CMD" = "-h" ]; then 13 | echo "USAGE" 14 | echo " $0 command" 15 | echo 16 | echo "EXAMPLES" 17 | echo 18 | echo " $0 ls -ltr # run ls -ltr inside the running container" 19 | echo " $0 make # run make inside the running container" 20 | exit 0 21 | fi 22 | container_id=`docker ps --no-trunc --filter ancestor=${ACCOUNT}/${REPO}:${TAG} --format "{{.ID}}"` 23 | 24 | if [ -z $container_id ]; then 25 | echo "😦 The container is not running. Try doing bin/start in another terminal" 26 | exit 1 27 | else 28 | echo "🚂 Running '$CMD' inside container with id '$container_id'" 29 | docker exec -it $container_id $CMD 30 | fi 31 | 32 | # vim: ft=bash 33 | -------------------------------------------------------------------------------- /bin/push: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -e 3 | 4 | SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )" 5 | 6 | . ${SCRIPT_DIR}/vars 7 | 8 | echo "[ push ] 🛳 Pushing ${ACCOUNT}/${REPO}:${TAG} to Docker Hub…" 9 | docker push ${ACCOUNT}/${REPO}:${TAG} 10 | -------------------------------------------------------------------------------- /bin/start: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | echo "🚀 Starting docker-compose.yml" 3 | docker compose up 4 | 5 | # vim: ft=bash 6 | -------------------------------------------------------------------------------- /bin/vars: -------------------------------------------------------------------------------- 1 | # Set this to the port in the Docker container you want exposed. 2 | # This should be the port your Rails app runs on, and 3000 is the default 3 | EXPOSE=3000 4 | 5 | # Set this to the port on your localhost you want to use to access 6 | # the the Rails app. 7 | LOCAL_PORT=9999 8 | 9 | # Docker/Docker Hub setup. This is here to allow pushing a built 10 | # image to Docker Hub and to ensure proper namespace isolation 11 | # of the image that's built by this repo 12 | # 13 | # Set this to your account name on Docker Hub (required) 14 | ACCOUNT=davetron5000 15 | # Set this to the repo name on Docker Hub (required) 16 | REPO=sustainable-rails-docker 17 | # Set this to the tag name to use (required) 18 | TAG=rails-7-arm64 19 | 20 | # Set this to the directory inside the Docker image you want to mirror 21 | # your project's root directory 22 | WORKDIR=/root/work 23 | 24 | # vim: ft=bash 25 | -------------------------------------------------------------------------------- /docker-compose.yml.template: -------------------------------------------------------------------------------- 1 | services: 2 | # This is the name of the container where our code 3 | # is going to run. The key can be anything. Note that this 4 | # key will be the name of this image on the network that docker-compose 5 | # creates, so make it explicit and obvious. 6 | %REPO%: 7 | # This is the image where our app runs. image: is a keyword. The value 8 | # is the name of a Docker image, and likely in the format ACCOUNT/REPO:TAG 9 | image: %ACCOUNT%/%REPO%:%TAG% 10 | # This tells Docker what to do when starting up. In the case of our app, we don't 11 | # want it to pull from a registry unless it's not here. 12 | pull_policy: "missing" 13 | 14 | # This says that our app should not start up until the DB and Redis instances 15 | # are up. Ideally we'd use "service_healthy" which says to wait for the services to 16 | # pass a health check, but neither Postgres nor Redis docker images configure one. 17 | # So, we'll use "service_started" instead. Note that the keys inside depends_on: 18 | # must match the service names used after our app's stanza below, i.e. db: and redis: 19 | depends_on: 20 | db: 21 | condition: "service_started" 22 | redis: 23 | condition: "service_started" 24 | 25 | # The Docker documentation is unclear as to what this does, but it seems to speed up 26 | # shutdown of the overall system. 27 | init: true 28 | # This maps the port we've exposed from inside 29 | # the Docker container to the given local 30 | # port on our host, i.e. requests to our 31 | # localhost for LOCAL_PORT will be served 32 | # by whatever is running in the container on EXPOSE 33 | ports: 34 | - "%LOCAL_PORT%:%EXPOSE%" 35 | - "22:22" 36 | 37 | # This maps the WORKDIR directory inside the Docker 38 | # container to our hosts directory, VOLUME_SOURCE 39 | # which is what allows us to share files between 40 | # our computer and the Docker container. 41 | # 42 | # The "delegated" consistency should make disk access faster 43 | # on macOS. This will cache reads and writes making the container 44 | # authoritative, so your computer will be behind, but not by much 45 | volumes: 46 | - type: bind 47 | source: "%VOLUME_SOURCE%" 48 | target: "%WORKDIR%" 49 | consistency: "delegated" 50 | entrypoint: /root/docker-entrypoint.sh 51 | # This creates a SECOND container 52 | # called "db" that will run Postgres. 53 | # This value here (db) will be the name 54 | # of the computer from the perspective of 55 | # the other Docker container, meaning 56 | # if we want to make a DB connection 57 | # to the Postgres running here, we'd 58 | # use the hostname db to do it 59 | db: 60 | # This is the name of the image 61 | # to run, and in this case, the image 62 | # will be fetched from DockerHub. 63 | # If you go to https://hub.docker.com/_/postgres 64 | # You can see how this works. 65 | # The "postgres" part tells Docker 66 | # to look in the postgres "docker repo". 67 | # 68 | # The 15 part is the "tag" inside that 69 | # repo, representing the image to fetch. 70 | # Fortunately, it's the same as the version 71 | # of Postgres we want. 72 | # 73 | # If you scroll down to the section titled 74 | # "Supported tags…", you'll see 75 | # the 13 tag and you can click through 76 | # to see the Dockerfile that built the image. 77 | # 78 | # Lastly, if you scroll down to the part 79 | # titled "Environment Variables", you 80 | # can see the default values for 81 | # connecting to postgres. 82 | image: arm64v8/postgres:15 83 | # We don't want to pull this big image every time 84 | pull_policy: "missing" 85 | # In Postgres 15, there is no default value for the password. 86 | # Since we're using this for dev, we need to set it 87 | # and it doesn't matter if it's 'secure', so we'll use 88 | # "postgres" as the password 89 | environment: 90 | POSTGRES_PASSWORD: postgres 91 | # Here, we set up Redis which we don't need to use in the book 92 | # until we start talking about background jobs. Like the 93 | # Postgres block above, this starts up a Redis server using 6.x.x 94 | redis: 95 | image: arm64v8/redis:6 96 | # We don't want to pull this big image every time 97 | pull_policy: "missing" 98 | # vim: ft=yaml nospell 99 | -------------------------------------------------------------------------------- /docker-entrypoint.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -e 4 | 5 | # Output some helpful messaging when invoking `bin/start` (which itself is 6 | # a convenience script for `docker compose up`. 7 | # 8 | # Adding this to work around the mild inconvenience of the `app` container's 9 | # entrypoint generating no output. 10 | # 11 | cat <<-'PROMPT' 12 | 🎉 Dev Environment Initialized! 🎉 13 | 14 | ℹ️ To use this environment, open a new terminal and run 15 | 16 | bin/exec bash 17 | 18 | 🕹 Use `ctrl-c` to exit. 19 | PROMPT 20 | 21 | # Using `sleep infinity` instead of `tail -f /dev/null`. This may be a 22 | # performance improvement based on the conversation on a semi-related 23 | # StackOverflow page. 24 | # 25 | # @see https://stackoverflow.com/a/41655546 26 | sleep infinity 27 | --------------------------------------------------------------------------------