M1 芯片 Mac 搭建 ruby 2.5.7 开发环境
电脑续航能力越来越差,内存、存储空间也不太够,索性换了一台二手的M1芯片电脑。作为一名 Ruby on Rails 开发者,首先要做的就是搭建开发环境,本文从直接搭建 ruby 环境和 Docker 搭建 ruby 环境两种方式来进行介绍。
目标
搭建 ruby 2.5.7 开发环境,并启动现有 Ruby on Rails 服务。
软硬件
系统:MacOS Sonoma 14.4.1
芯片:Apple M1
数据库:PostgreSQL 14
版本管理工具:asdf
背景知识
Rosetta 2 转义技术 能以一种变通的方式将为 Intel 处理器编译的程序运行于 Apple M1 芯片上。
方式一:直接搭建 ruby 环境
由于 ruby 2.6 及以下版本在安装 ARM 版时会有编译问题, 有些 ruby gem 比如 ruby-oci8
需要 x86 版 ruby 才能安装,这里就介绍如何在 ARM 架构芯片电脑上搭建 x86 版 ruby 环境。
- 鼠标移到 iTerm 终端图标上,右键复制一个新的终端出来后,命名为 iTerm x86_64。
- iTerm x86_64 图标右键,选择 Get info, 勾选 Open using Rosetta,重新启动终端。
- 通过
arch -x86_64
前缀来安装 x86 版 homebrew。
# https://brew.idayer.com/guide/m1/#安装-x86-版-homebrew
$ arch -x86_64 /bin/bash -c "$(curl -fsSL <https://gitee.com/ineo6/homebrew-install/raw/master/install.sh>)"
# 设置别名,与 ARM 版 Homebrew 区分开
$ alias ibrew='arch -x86_64 /usr/local/bin/brew'
- 安装openssl等工具:
$ ibrew install openssl@1.1 readline libyaml gmp
- 设置环境变量:
export RUBY_CONFIGURE_OPTS="--with-openssl-dir=$(ibrew --prefix openssl@1.1) --with-readline-dir=$(ibrew --prefix readline) --with-libyaml-dir=$(ibrew --prefix libyaml)"
- 安装ruby版本:
$ asdf install ruby 2.5.7
如果是安装ruby 3.1即以上版本,则需要用到
openssl@3.x
此时需要重新设置环境变量:
$ export RUBY_CONFIGURE_OPTS="--with-openssl-dir=$(ibrew --prefix openssl@3) --with-readline-dir=$(ibrew --prefix readline) --with-libyaml-dir=$(ibrew --prefix libyaml)"
- 接下来进入项目目录,执行以下命令安装
bundler
和gemfile.lock
中的插件:
$ gem install bundler:version # version 替换成实际版本
$ bundle install
在这个过程中可能会遇到一些问题。
1、安装pg插件报错:
Unable to find PostgreSQL client library.
Please install libpq or postgresql client package like so:
brew install libpq
那是因为 PostgreSOL 软件安装的是 ARM 架构版的,需要重新安装 x86 版的。
# 原先 ARM 版安装的 homebrew 没有使用别名,还是用 brew 命令
$ brew services stop postgresql@14(brew 对应 ARM 版本的)
$ brew uninstall postgresql@14
$ ibrew install postgresql@14(ibrew 对应 x86 版本的)
$ ibrew services start postgresql@14
# 以下命令验证 pg 插件是否能成功安装
$ gem install pg
# pg 插件可以成功安装之后,再次执行命装其余插件
$ bundle install
2、如果执行 rails console
报错:connection to server on socket "/tmp/.s.PGSQL.5432" failed: No such file or directory
,说明 PostgreSQL 没有启动成功。
执行 ibrew services info postgresql@14
查看执行情况,显示如下,果然没在运行。
postgresql@14 (homebrew.mxcl.postgresql@14)
Running: ✘
Loaded: ✔
Schedulable: ✘
查看日志 tail -f /usr/local/var/log/postgresql@14.log
发现报错信息: postgres: could not access the server configuration file "/usr/local/var/postgresql@14/postgresql.conf": No such file or directory
原来是没有初始化db, /usr/local/var/postgresql@14
目录中没有任何文件,执行 initdb -D /usr/local/var/postgresql@14
即可。
3、执行 initdb -D /usr/local/var/postgresql@14
报错:
running bootstrap script ... 2024-07-12 23:30:49.882 CST [82449] FATAL: could not create shared memory segment: Cannot allocate memory
2024-07-12 23:30:49.882 CST [82449] DETAIL: Failed system call was shmget(key=8862685, size=56, 03600).
2024-07-12 23:30:49.882 CST [82449] HINT: This error usually means that PostgreSQL's request for a shared memory segment exceeded your kernel's SHMALL parameter. You might need to reconfigure the kernel with larger SHMALL.
这通常是因为 PostgreSQL 请求的共享内存段超出了系统的 SHMALL 参数。
# 执行命令:
sysctl kern.sysv.shmmax
sysctl kern.sysv.shmmin
sysctl kern.sysv.shmmni
sysctl kern.sysv.shmseg
sysctl kern.sysv.shmall
# 可以看到返回以下内容:
kern.sysv.shmmax: 4194304
kern.sysv.shmmin: 1
kern.sysv.shmmni: 32
kern.sysv.shmseg: 8
kern.sysv.shmall: 1024
在 macOS 上,可以通过修改 /etc/sysctl.conf 文件来调整共享内存设置。如果文件不存在,可以创建它: sudo vi /etc/sysctl.conf
,然后添加以下内容:
kern.sysv.shmmax: 4194304
kern.sysv.shmmin: 1
kern.sysv.shmmni: 32
kern.sysv.shmseg: 8
kern.sysv.shmall: 65536
改了kern.sysv.shmall, 1024 -> 65536
保存修改后重启系统,新的配置才会生效,可以重新执行 initdb,并重启 PostgreSQL 了。
4、正常到这一步就可以启动 rails console
了,但是有些项目还会遇到如下报错:
connection to server on socket "/tmp/.s.PGSQL.5432" failed: could not create socket: Too many open files (ActiveRecord::ConnectionNotEstablished)
一开始以为是句柄数不够,通过 ulimit -n 句柄数
增加数量,但是没有用。
后来执行了 rails dbconsole
,错误提示发生了变化:psql: error: connection to server on socket "/tmp/.s.PGSQL.5432" failed: FATAL: database "tc_mini_site_development" does not exist
。
原来是因为没有创建数据库…
执行 rails db:setup
创建并初始化数据库就好了。
方式二:Docker 搭建 ruby 环境
安装图形界面版 Docker: brew install docker --cask
项目根目录添加文件 Dockerfile
、docker-entrypoint.sh
、sidekiq-entrypoint.sh
、 docker-compose.yml
和 .env
。
Dockerfile
文件:
# 指定基础镜像为ruby:2.5.7,所有操作将基于此镜像。
FROM ruby:2.5.7
# 设置环境变量LANG为C.UTF-8,用于支持UTF-8编码。
ENV LANG=C.UTF-8
# 设置环境变量BUNDLER_VERSION为2.0.2,用于指定Bundler的版本。
ENV BUNDLER_VERSION=2.0.2
# 更新APT包索引,并升级所有已安装的包到最新版本。
RUN apt-get update && apt-get upgrade -y
# 使用APT安装构建和开发Ruby应用所需的各种依赖库以及工具,并避免安装不必要的推荐包。
RUN apt install -y build-essential libssl-dev libpq-dev libxml2-dev libxslt1-dev \\
git imagemagick libbz2-dev libjpeg-dev libevent-dev libmagickcore-dev \\
libffi-dev libglib2.0-dev zlib1g-dev libyaml-dev vim --no-install-recommends
# 安装Node.js和Yarn包管理器,用于管理前端依赖。
RUN apt install -y nodejs yarn
# 安装指定版本的Bundler。
RUN gem install bundler -v 2.0.2
# 设置工作目录为/inf-idea-backend,后续命令将在该目录下执行。
WORKDIR /inf-idea-backend
# 将当前目录下的Gemfile和Gemfile.lock文件复制到容器的工作目录中。
COPY Gemfile Gemfile.lock ./
# 运行Bundler安装Gemfile中的所有依赖。
RUN bundle install
# 将当前目录下的所有文件复制到容器的工作目录中。
COPY . ./
# Add a script to be executed every time the container starts.
# 指定容器启动时执行的入口点脚本。
ENTRYPOINT ["./docker-entrypoint.sh"]
docker-entrypoint.sh
文件:
#!/bin/bash
# 指定脚本使用的解释器为bash。
# 当命令执行出错时,立即退出脚本。
set -e
# 检查tmp/pids/server.pid文件是否存在,如果存在则删除它,以防止Rails服务器在启动时因PID冲突而无法正常启动。
if [ -f tmp/pids/server.pid ]; then
rm tmp/pids/server.pid
fi
# 执行数据库迁移,将数据库结构更新为最新版本。
bundle exec rails db:migrate
# 启动Rails服务器,并将其绑定到所有可用的网络接口(0.0.0.0)。
bundle exec rails s -b 0.0.0.0
sidekiq-entrypoint.sh
文件:
#!/bin/sh
set -e
bundle exec sidekiq
docker-compose.yml
文件
services:
docker_app:
tty: true # 为容器分配一个伪终端,便于调试。
stdin_open: true # 保持标准输入打开,以便可以在容器中进行交互操作。
build:
context: . # 构建镜像时,使用当前目录作为上下文。
dockerfile: Dockerfile # 使用当前目录中的Dockerfile文件构建镜像(默认行为)。
depends_on: # 指定依赖的服务,确保在启动`docker_app`服务之前,`docker_database`和`docker_redis`服务已启动。
- docker_database
- docker_redis
ports:
- "3000:3000" # 将容器的3000端口映射到主机的3000端口。
volumes:
- .:/inf-idea-backend # 将当前目录挂载到容器内的`/inf-idea-backend`路径下。
- gem_cache:/usr/local/bundle/gems # 将名为`gem_cache`的卷挂载到容器内的`/usr/local/bundle/gems`路径下,用于缓存Gem依赖。
- node_modules:/inf-idea-backend/node_modules # 将名为`node_modules`的卷挂载到容器内的`/inf-idea-backend/node_modules`路径下,用于缓存Node.js依赖。
env_file: .env # 加载`.env`文件中的环境变量。
environment:
RAILS_ENV: development # 设置环境变量`RAILS_ENV`为`development`,表示使用开发环境。
docker_database:
image: postgres:14.11 # 使用`postgres:14.11`镜像作为数据库服务。
environment:
- "POSTGRES_HOST_AUTH_METHOD=trust" # 设置环境变量,允许Postgres使用信任认证方式,无需密码登录。
volumes:
- db_data:/var/lib/postgresql/data # 将名为`db_data`的卷挂载到容器内的`/var/lib/postgresql/data`路径下,用于持久化数据库数据。
- ./init.sql:/docker-entrypoint-initdb.d/init.sql # 将当前目录中的`init.sql`文件挂载到容器内的`/docker-entrypoint-initdb.d/init.sql`路径下,用于初始化数据库。
docker_redis:
image: redis:7.2.4 # 使用`redis:7.2.4`镜像作为Redis服务。
docker_sidekiq:
build:
context: . # 构建镜像时,使用当前目录作为上下文。
dockerfile: Dockerfile # 使用当前目录中的Dockerfile文件构建镜像。
depends_on: # 指定依赖的服务,确保在启动`docker_sidekiq`服务之前,`docker_app`、`docker_database`和`docker_redis`服务已启动。
- docker_app
- docker_database
- docker_redis
volumes:
- .:/inf-idea-backend # 将当前目录挂载到容器内的`/inf-idea-backend`路径下。
- gem_cache:/usr/local/bundle/gems # 将名为`gem_cache`的卷挂载到容器内的`/usr/local/bundle/gems`路径下,用于缓存Gem依赖。
- node_modules:/app/node_modules # 将名为`node_modules`的卷挂载到容器内的`/app/node_modules`路径下,用于缓存Node.js依赖。
env_file: .env
environment:
RAILS_ENV: developmen
entrypoint: ./sidekiq-entrypoint.sh # 指定容器启动时执行的入口点脚本为`./sidekiq-entrypoint.sh`。
volumes:
gem_cache: # 定义一个名为`gem_cache`的卷,用于缓存Gem依赖。
db_data: # 定义一个名为`db_data`的卷,用于持久化数据库数据。
node_modules: # 定义一个名为`node_modules`的卷,用于缓存Node.js依赖。
.env
文件添加环境变量配置 Docker 容器中的 Redis 和 Postgres 服务:
DATABASE_NAME=
DATABASE_USERNAME=postgres
DATABASE_PASSWORD=
DATABASE_HOST=docker_database
REDIS_SERVER=docker_redis
REDIS_PORT=6379
REDIS_DB_NUM=0
database.yml
文件使用环境变量配置数据库:
<% database = Rails.application.credentials.dig(Rails.env, :database) %>
...
development:
<<: *default
database: <%= ENV["DATABASE_NAME"] || database.dig(:name) %>
username: <%= ENV["DATABASE_USERNAME"] || database.dig(:username) %>
password: <%= ENV["DATABASE_PASSWORD"] || database.dig(:password) %>
host: <%= ENV["DATABASE_HOST"] || '127.0.0.1' %>
username: <%= ENV["DATABASE_USERNAME"] || 'postgres' %>
...
Redis 服务配置也一样需要换成上面定义的环境变量,这里就不赘述。
使用命令docker compose up --build
同时启动四个服务。
如果只想启动一个服务,可以在 up
后面加上服务名称,比如docker compose up -d docker_app --build
, 其中 -d
选项表示后台运行。也可以直接在 Docker 应用界面上点击启动按钮。
--build
参数表示构建环境,第一次启动时或者有添加新的 ruby gem 时,需要添加该参数,其他时候可省略。
执行 docker compose exec docker_app bash
进入容易 shell 交互环境,此时可执行 rails db:migrate
、 rails console
等命令。
总结
一开始使用 Docker 搭建方式启动项目,用了一段时间后,发现用起来不是很方便,后来还是使用了直接安装ruby搭建环境的方式。
使用 Docker 容器的话,shell 环境用的就不是用本地的了:
1、用不了本地终端安装的便捷工具,比如 Oh My Zsh
,还有 z
、 zsh-autosuggestions
、rails
等插件。本地可以配置并通过简洁命令 rc
移动 rails console,而 docker 中就是原生的bash终端环境,如果想要使用相同的便捷操作,只能再配置一遍。
2、为了能在 docker compose exec docker_app bash
,即 Docker 容器 shell 中执行 cap staging rails console
以连接远端测试服务器,需要把本地的 id_rsa、id_rsa.pub 和 beansmile.ssh (自主配置连接信息文件) 放到项目目录中,目录结构如下
然后在 Dockerfile
文件中添加以下脚本,将内容复制到 docker WORKDIR 相应文件夹中才行。
# 设置SSH相关配置。
# 在用户主目录下创建.ssh目录。
RUN mkdir ~/.ssh
# 将私钥id_rsa复制到.ssh目录中。
RUN cp ssh/id_rsa ~/.ssh/
# 将公钥id_rsa.pub复制到.ssh目录中。
RUN cp ssh/id_rsa.pub ~/.ssh/
# 将当前目录的ssh_config目录下的所有文件复制到.ssh/ssh_config/目录中。
RUN cp -R ssh/ssh_config/ ~/.ssh/ssh_config/
# 创建一个空的SSH配置文件。
RUN touch ~/.ssh/config
# 向SSH配置文件中写入配置,以包含特定的SSH设置。
RUN echo 'Include ./ssh_config/beansmile.ssh' > ~/.ssh/config
3、 Docker 容器 shell 中只能使用 vim 编辑器打开 credentials 文件,而本地可以使用 sublime 等有图形界面的编辑器打开。
参考文档
Containerizing a Ruby on Rails Application for Development with Docker Compose