Installation¶
Two supported paths. Use Docker unless you have a reason not to: it is the same Passenger + Nginx stack that runs in production, and it brings its own MySQL and Redis.
- Docker -- one command to a running app; everything except the source tree is disposable.
- Native -- Ruby, MySQL and Redis on your own machine. Faster test runs, more setup.
Docker¶
Prerequisites¶
- Docker Compose 2.24 or newer (
docker compose version).docker-compose.ymluses the long-formenv_file:withrequired: false, which older versions reject. - Typically around 4 GB of free RAM and a few GB of disk for the image.
- Free ports on loopback: 8080, 3306, 6379 -- all three are overridable, see Ports.
First run¶
git clone git@github.com:jwechsler/stagemgr.git
cd stagemgr
cp .env.example .env
docker compose up -d --build
docker compose exec -u app stagemgr bundle exec rake setup:wizard
docker compose up -d # pick up the SECRET_KEY_BASE the wizard wrote
Then open http://localhost:8080/login and sign in with the administrator credentials you chose.
The first boot installs gems and JavaScript packages and loads the schema, so
give it several minutes; docker compose logs -f stagemgr shows where it is,
and it ends with ==> Boot complete (development).
cp .env.example .env is optional -- the entrypoint runs rake setup:config,
which creates .env and the config/*.yml files from their tracked .example
templates on the first boot. Copying it first only means Compose can read your
values on that same first boot.
Why the second up -d
setup:wizard writes a generated SECRET_KEY_BASE (and any Stripe keys you
give it) into .env. Compose only reads .env when it creates a
container, and dotenv never overwrites a variable that is already set -- so
the running container still has the blank value it started with, and
docker compose restart does not change that. docker compose up -d
recreates the container with the new environment. Until you do,
setup:doctor reports secret_key_base: ENV[...] is present but BLANK and
sessions do not survive a restart.
Always -u app
docker compose exec runs as root by default, and anything root writes
into the bind mount (log/, tmp/, public/packs) is then owned by root
on your host too. -u app runs as the same user Passenger and the Resque
workers use. Gems resolve either way -- the image puts the app's RVM gemset
on PATH, GEM_HOME and GEM_PATH for non-login shells, which is what
docker compose exec uses.
What the wizard asks¶
Each step is also a rake task of its own, and all of them are idempotent.
| Step | Prompt | Default | Skipped by |
|---|---|---|---|
| Session signing key | none | -- | -- |
| Database | none | -- | -- |
| Administrator account | email, then password (not echoed) | none -- a value is required | ADMIN_EMAIL, ADMIN_PASSWORD |
| Theater and venue | theater name, primary venue name | the existing Default theater / first venue, else My Theater / Main Stage |
-- |
| Site theme | theme slug | blank -- press Enter to skip and use the generic copy | -- |
| Payment processing | "Configure Stripe keys now?", then the two keys | Y |
answer n |
| Demo data | "Create sample demo data?" | Y |
answer n |
| Health check | none -- runs setup:doctor |
-- | -- |
Only the administrator step reads environment variables, so a scripted install still has to feed the rest on stdin:
printf 'My House\nMain Stage\n\nn\ny\n' | docker compose exec -T -u app \
-e ADMIN_EMAIL=you@example.org -e ADMIN_PASSWORD=a-real-password \
stagemgr bundle exec rake setup:wizard
The seeded placeholders
setup:bootstrap seeds an empty database, and db/seeds.rb creates an
administrator admin@yourtheater.com whose password is published in this
repository, plus a Default theater row named Theater 1 (which would
otherwise become your house's name everywhere). The wizard replaces both:
setup:admin renames the placeholder account while it is still the only
administrator, and setup:theater renames the existing Default row rather
than adding a second one. If you skip the wizard, run those two tasks --
and check User.where(is_administrator: true).pluck(:email) before letting
anyone reach the install.
What the container does on every start¶
bin/docker-entrypoint is idempotent and overwrites nothing that already
exists:
- Optionally renumbers the
appuser to matchHOST_UID/HOST_GID(Linux bind mounts). bundle install.rake setup:config-- scaffoldsconfig/*.ymland.envfrom the templates.- Production only:
rake setup:doctor, and refuses to serve if it fails. yarn installandrails webpacker:compile(plusassets:precompilein production).rake setup:bootstrap-- create the database, loaddb/schema.rb, migrate, seed if there are no users. Controlled byDB_PREPARE_ON_BOOT.- Installs the Nginx site config: foundation mode if
/var/www/foundation-distis mounted, standalone otherwise. execs/sbin/my_init, which starts Nginx, Passenger and the runit-managed Resque worker and scheduler.
In development every step is guarded: a failure is reported and the container
stays up so you can exec in and fix it. In production every step is fatal. The
one exception is bundle install, which is fatal in every environment -- nothing
after it can run without gems, so continuing would replace one clear error with
a page of confusing ones.
Rebuild after editing the image's own files
bin/docker-entrypoint, docker/nginx.conf, docker/nginx.foundation.conf
and docker/service/** are baked into the image, not bind-mounted.
Changing one needs docker compose build (or up -d --build). Everything
under app/, config/, lib/ and sites/ is live in the bind mount and
needs no rebuild.
Everyday commands¶
docker compose logs -f stagemgr # boot output, Passenger, workers
docker compose exec -u app stagemgr bash # a shell as the app user
docker compose exec -u app stagemgr bundle exec rails c
docker compose exec -u app stagemgr bundle exec rake setup:doctor
docker compose exec -u app stagemgr bundle exec rspec spec/models/theater_info_spec.rb
docker compose restart stagemgr # re-runs the entrypoint, same environment
docker compose up -d # recreate after editing .env
docker compose down # stop; data survives
docker compose down -v # stop and destroy the databases
The Resque worker and scheduler run as runit services inside the container, so
they restart with it. stop_grace_period: 30s in docker-compose.yml gives a
running job time to finish before Docker escalates to SIGKILL.
Ports and a second stack¶
Published ports bind to 127.0.0.1 by default. This is a development stack
with development credentials and it has no business being reachable from the
LAN; set STAGEMGR_BIND=0.0.0.0 deliberately if you need it from another
device.
COMPOSE_PROJECT_NAME=smoke STAGEMGR_PORT=18080 MYSQL_PORT=13306 \
REDIS_PORT=16379 docker compose up -d
Two MySQL gotchas¶
Credentials are baked into the data volume
The mysql image applies MYSQL_ROOT_PASSWORD, MYSQL_USER and
MYSQL_PASSWORD the first time the volume initializes and never again.
Changing DATABASE_USER/DATABASE_PASSWORD in .env afterwards produces
Access denied until you either docker compose down -v (which destroys
the data) or ALTER USER by hand.
DATABASE_USER must not be root: the image refuses to start with
MYSQL_USER=root. MYSQL_ROOT_PASSWORD is deliberately a separate variable --
the application never needs root, and reusing one password widens the blast
radius of a leaked .env.
docker/mysql-init/10-test-grants.sh grants the application account every
database matching stagemgr_test%, so rspec and cucumber can create their
own databases (and a per-worktree TEST_DATABASE_NAME works). Like everything
in /docker-entrypoint-initdb.d, it runs only on a fresh volume. On a volume
that already exists, run the grant once by hand:
docker compose exec mysql \
mysql -uroot -pchangeme-root \
-e "GRANT ALL ON \`stagemgr\_test%\`.* TO 'stagemgr_dev'@'%';"
changeme-root is the default MYSQL_ROOT_PASSWORD from docker-compose.yml
(commented out in .env.example; set it there to change it), and stagemgr_dev
is the default DATABASE_USER. Substitute your own if you changed either --
and note the values are whatever the volume was initialized with, not
whatever .env says now.
What lives where¶
- The checkout is bind-mounted at
/var/www/stagemgr.config/*.yml,.env, encryption keys andsites/<slug>/are read straight out of your working tree; nothing is baked into the image. - Gems live in the image's RVM gemset (
ruby-3.2.2@stagemgr), backed by thebundle-cachenamed volume -- never invendor/bundle, so the container never writes into the checkout. node_modulesis a named volume too: it has to sit in-tree for yarn/webpacker resolution, but the builds are Linux ones and must not overwrite your host's.
On Linux, files the container writes into the bind mount (log/, tmp/,
public/packs) come back owned by uid 9999. Set HOST_UID and HOST_GID in
.env to your own id -u/id -g and the entrypoint renumbers its app user
to match. Unnecessary on Docker Desktop.
Running the container in production mode¶
Set RAILS_ENV=production and the entrypoint precompiles assets and gates the
boot on rake setup:doctor; DB_PREPARE_ON_BOOT defaults to false there,
because schema changes belong to a deploy step rather than to a container
restart. The image is documented as a development and CI image -- read
Deployment before relying on it to serve
patrons.
Native install (macOS / Linux)¶
Best for working on the application itself: rspec and cucumber run directly
against your machine's MySQL and Redis with no container round-trip.
1. Runtime dependencies¶
A typical set; package names vary by distribution and release.
brew install mysql@8.0 redis vips libxml2 libxslt openssl@3
brew services start mysql@8.0
brew services start redis
sudo apt-get install -y mysql-server redis-server \
libvips libvips-dev default-libmysqlclient-dev \
libxml2-dev libxslt1-dev build-essential
libvips is not optional: ruby-vips loads libvips.so at boot, so the app
will not start without it.
2. Ruby and Node¶
Ruby 3.2.2 (.ruby-version) and Node 22 (.nvmrc), under whichever
version manager you use:
rbenv install 3.2.2 && rbenv local 3.2.2 # or asdf / rvm
nvm install 22 && nvm use 22
gem install bundler:2.4.10 # the version in Gemfile.lock
npm install -g yarn
3. Clone and configure¶
git clone git@github.com:jwechsler/stagemgr.git
cd stagemgr
bundle install
yarn install
bundle exec rake setup:config # config/*.yml and .env from the templates
.env.example's defaults are native-friendly -- DATABASE_HOST=127.0.0.1,
REDIS_URL=redis://127.0.0.1:6379/0. Edit .env for your MySQL account.
In the config/server.yml this generates, leave sub_uri: blank unless the
app is genuinely mounted under a path: it is folded into every mailed URL, so a
stray /tickets sends patrons to a 404. Then:
bundle exec rake setup:wizard
Do not put Docker hostnames in .env
A .env carrying DATABASE_HOST=mysql or REDIS_URL=redis://redis:6379/0
breaks every native bin/rails and rake command with
Redis::CannotConnectError or a MySQL connection error. The Compose stack
sets those two names itself, in environment:, which beats env_file: --
so leave the loopback values in .env and both paths work. See
Troubleshooting.
4. Run it¶
Three processes:
bundle exec rails server -p 8080 # Puma
bundle exec rake environment resque:work QUEUE='*' # background jobs
bundle exec rake environment resque:scheduler # recurring jobs
Browse to http://localhost:8080/login.
Puma stays in the Gemfile for exactly this -- rails server on a native
install. Passenger is what serves the app in Docker and in production.
The Theater Wit workspace¶
Theater Wit develops Stagemgr alongside a separately built Foundation marketing
site, in a workspace of peer checkouts (site/, stagemgr/, src/, dist/,
tktprint/). Two things differ from a plain install:
- Foundation mode.
../distis mounted into the container at/var/www/foundation-dist. The entrypoint then installsdocker/nginx.foundation.conf(static files at/, Rails under/tickets), waits up to 30 seconds forlayouts/ext_site_wrapper.htmlto appear in that build, and symlinks it toapp/views/layouts/ext_site_wrapper.html.erb-- Rails needs a handler extension on a layout and the Foundation build emits plain.html.config/server.ymlmust setsub_uri: /ticketsandext_site_wrapper: ext_site_wrapperto agree with it. site/'s Compose file layers on top of this one.stagemgr'sdocker-compose.ymlis the primary stack; the workspace adds thefoundationservice and Theater Wit's own mounts on top of it rather than defining a second copy of the app.
A standalone install wants none of it: no foundation-dist mount,
ext_site_wrapper: standalone, a blank sub_uri:, and Rails at /. If a foundation-mode symlink
is left behind from an earlier run, the entrypoint removes it -- a dangling
layout is worse than none, because every public page would 500.