Deployment¶
The reference production deployment is bare metal: a flat git checkout at
~/stagemgr, served by Phusion Passenger, with the Resque worker and scheduler
started as ordinary background processes. This page documents that path first,
because it is the one in daily use, and then what a container-based production
would need.
The shape of a production box¶
| Piece | How |
|---|---|
| Web | Passenger, restarted by touch tmp/restart.txt |
| Static files | The web server serves public/ directly -- config.public_file_server.enabled is off unless RAILS_SERVE_STATIC_FILES is set, and config.assets.compile is false, so assets must be precompiled at deploy time |
| Background jobs | script/resque-worker start -- three workers in production, split by queue |
| Recurring jobs | script/scheduler start (config/schedule.yml) |
| Configuration | config/server.yml on the box (gitignored) |
| Secrets | Encrypted credentials plus config/master.key on disk |
| Reports | db/reports/*.sh from cron |
bin/deploy¶
ssh yourbox 'stagemgr/bin/deploy'
It runs the deploy sequence in a fixed order so no step can be forgotten:
git checkout masterandgit pull --ff-onlybundle check || bundle installyarn install --frozen-lockfilerails db:migraterake setup:doctor-- fails the deploy here, while the old code is still serving, if a required secret or config file is missingrails assets:precompiletouch tmp/restart.txt(Passenger)script/resque-worker restartscript/scheduler stop || truethenscript/scheduler start- A
curlsmoke check againstSMOKE_URL
Every step is idempotent, so a failed deploy is retried by running the script
again. The smoke check doubles as the request that boots the new code under
Passenger, so a boot crash -- a missing gem, a missing secret -- fails the deploy
there instead of surfacing to the first customer. Each run appends a line to
log/deploys.log.
Two knobs: SMOKE_URL (defaults to Theater Wit's login page) and
FORCE_DEPLOY=1, which lifts the guard that refuses to run anywhere but
$HOME/stagemgr -- the development tree must never receive production rake
tasks.
The doctor is the deploy gate
bin/deploy runs rake setup:doctor after migrating and before
precompiling, so a broken install fails before touch tmp/restart.txt
rather than after. You can also run it by hand at any time:
RAILS_ENV=production bundle exec rake setup:doctor
rake setup:doctor is one of the tasks exempt from the production
required-secrets check, so it
can run on a box that is not configured yet and tell you what is wrong.
Restricting the production bundle¶
Configure Bundler once on the box:
bundle config set --local without 'development test cucumber'
That writes .bundle/config in the app directory and persists across deploys.
Without it, any gem later added to the :development, :test or :cucumber
group has to be installed on the production box too or the app fails to boot
with Bundler::GemNotFound. The application itself is unaffected --
config/application.rb requires only the :default and :production groups
there.
It is also why dotenv is not available in production: it is a
development/test-group gem. A .env file on a bare-metal production box is read
by nothing.
Confirm with bundle config (the without list should show all three groups)
and bundle check.
Background workers¶
script/resque-worker {start|stop|status|restart}
script/scheduler {start|stop}
In production script/resque-worker start launches three workers, each with its
own pidfile and log:
| Queues | Log |
|---|---|
notification,report,import,sync |
log/resque.log |
printing,batch_printing |
log/printing_queue.log |
default,maintenance |
log/maintenance_queue.log |
Outside production it starts a single worker on QUEUE=*, honouring whatever
RAILS_ENV you exported.
Workers need the secrets too
Workers boot through rake environment resque:work, and resque: tasks are
the one rake prefix the required-secrets check
still enforces. A worker without secrets is as broken as a web process -- it
delivers all the mail.
Serving under a sub-path¶
Theater Wit serves the app at /tickets under a marketing site. Three things
must agree:
| Where | Value |
|---|---|
config/server.yml |
sub_uri: "/tickets" |
| The process environment | RAILS_RELATIVE_URL_ROOT=/tickets (Passenger sets this from passenger_base_uri; Compose passes STAGEMGR_SUB_URI through) |
| The web server | passenger_base_uri /tickets, as docker/nginx.foundation.conf does |
sub_uri is passed to ActionMailer as :script_name rather than being folded
into :host. Route helpers only know about the mount when the process exports
RAILS_RELATIVE_URL_ROOT, and the Resque worker that delivers most of the mail
has no such environment -- passing :script_name explicitly settles it for every
process, so the prefix appears exactly once whether mail is delivered from a
worker or inline from a web request.
Credentials on the box¶
RAILS_ENV=production bin/rails credentials:edit --environment production
The box needs the matching key: config/credentials/production.key (or
config/master.key for the shared pair) on disk, or RAILS_MASTER_KEY in the
Passenger and worker environments. Both .key files are gitignored --
never commit one. See Credentials & secrets.
Standing up a new production box¶
Beyond the checklist below, a brand-new install has two placeholders to
clear. setup:bootstrap seeds an empty database, and db/seeds.rb creates:
| Placeholder | Why it matters |
|---|---|
Administrator admin@yourtheater.com |
Its password is published in this repository |
Default theater row Theater 1 |
Theater.default_theater.name is the house name on every public page and in every email |
RAILS_ENV=production bundle exec rake setup:admin # renames the placeholder account
RAILS_ENV=production bundle exec rake setup:theater # renames the Default row
setup:admin rewrites the placeholder only while it is still the only
administrator; once a real one exists it creates a new account instead.
setup:theater always updates the existing Default row rather than adding a
second one -- a second Default row would be inert, because default_theater is
the oldest. Verify before opening the doors:
RAILS_ENV=production bundle exec rails runner \
'puts User.where(is_administrator: true).pluck(:email).inspect, Theater.default_theater&.name'
Also set sub_uri: in config/server.yml to match how the web server actually
mounts the app -- blank when Rails owns /, /tickets when it does not. It is
folded into every mailed URL.
Before deploying at an existing install¶
A checklist for the first deploy of a Stagemgr carrying site themes, the
theater: block and the AppSecrets accessor.
- Add the new
config/server.ymlkeys on the box (the file is gitignored, so nothing arrives with the deploy):site_theme:-- your theme slug, if you have one- the whole
theater:block (Theming) my_emma: newsletter_group: / coupon_group:-- set these explicitly if you rely on names other thanNewsletterandFlash Offersemail: addresses: exception_notifications:-- otherwise crash reports fall back tosoftware_address
- Confirm the Default theater row's
nameis the house name you want in every email. It now drives all proper-noun copy. - Check the process environment for stale secret variables. The
environment now beats credentials, which is a reversal for Postmark and
MyEmma:
Anything set must be non-blank and equal to the credential, or the winner changes at the next restart. Delete blank lines rather than emptying them.
env | grep -E 'POSTMARK|STRIPE|MY_EMMA|RESQUE_ADMIN|SECRET_KEY_BASE' - Set
REPORT_EMAIL(and optionallyREPORT_SUBJECT_TAG) in the cron environment --db/reports/mail_*.shnow abort without it rather than mailing a placeholder address. - Run the doctor and read every line:
RAILS_ENV=production bundle exec rake setup:doctor - Only then deploy and restart.
Reports from cron¶
db/reports/mail_*.sh build a text report with mysql and mail it with mutt
or mailx. They read:
| Variable | Default | Purpose |
|---|---|---|
REPORT_EMAIL |
none -- the script aborts | Recipient and envelope sender |
REPORT_SUBJECT_TAG |
StageMgr |
Subject prefix, e.g. [StageMgr] Unfulfilled Flexpasses |
MY_CNF |
$HOME/.my.cnf |
MySQL defaults file used by the HUD scripts in db/reports/hud/ |
There is deliberately no default recipient: a report quietly mailed to a placeholder address is worse than a cron job that fails loudly.
Docker in production¶
The image can run with RAILS_ENV=production -- the entrypoint precompiles
assets, gates the boot on rake setup:doctor and defaults DB_PREPARE_ON_BOOT
to false -- but docker/Dockerfile describes itself as built for development
and CI, and the shipped docker-compose.yml is a development stack. Before
serving patrons from it, expect to supply at least:
- Secrets from the deploy environment, not from a bind-mounted
.env(environment:in a production override file, or Docker/Swarm secrets read as files and exported). - TLS and a public reverse proxy in front of the container. The image
listens on plain HTTP on port 80 and
config.force_sslis commented out inconfig/environments/production.rb. - A published port that is not loopback-only, deliberately
(
STAGEMGR_BIND), or a proxy on the same Docker network. - A managed database. The
mysqlservice here has a development password scheme and bakes its credentials into a local volume; production data wants a real MySQL with backups. - Log and asset handling.
RAILS_LOG_TO_STDOUT=truefor a container log driver; a plan forpublic/assetsandpublic/packs, which are precompiled into the bind mount at boot. - A migration step outside the container lifecycle, since
DB_PREPARE_ON_BOOTis off there by design.