Deploying to Scalingo
Trek apps deploy to Scalingo with the dedicated trek-buildpack, a single buildpack that replaces the usual apt + ruby buildpack pair and knows how a Trek app is built.
What the buildpack does
- installs the APT packages listed in
Aptfile(image processing libraries, jemalloc), skippingapt-get updateand package resolution entirely when theAptfileis unchanged - installs Ruby with rv — precompiled, in seconds — using the version pinned in
.ruby-version - installs Node.js pinned by
.node-version(generated byrails new), and the exact Yarn release pinned bypackage.json'spackageManagerfield — no unpinned defaults - runs
yarn installexactly once, with a persistent cache, beforeassets:precompile - slims the shipped image:
node_modules, Yarn caches and Node.js itself are dropped after the build, roughly halving the image size of a typical Trek app — faster deploys, restarts and scaling
Compared with the stock buildpacks, deploys typically go from around 3 minutes to about 1 minute 30 with a warm cache.
Set up the app
Generate the Scalingo files (an Aptfile and a Procfile with the web process and post-deploy migrations). The generator also comments out the kamal gem rails new adds, which drops its SSH dependencies from the bundle:
bin/rails g trek:scalingoThen configure the Scalingo app's environment, pinning the buildpack to a release through its archive:
scalingo --app <app> env-set BUILDPACK_URL=https://github.com/etaminstudio/trek-buildpack/archive/refs/tags/v0.1.4.tar.gz
scalingo --app <app> env-set 'DATABASE_URL=$SCALINGO_POSTGRESQL_URL'
scalingo --app <app> env-set RAILS_MASTER_KEY=$(cat config/master.key) > /dev/null
env-setprints every value it sets, so without> /dev/nullthe master key ends up in the terminal's scrollback and in any log or transcript of the session. Do the same for every secret below.
Pin through the archive, not a
#tagsuffix: Scalingo reads#refonBUILDPACK_URLas a branch, so…/trek-buildpack#v0.1.4asks for a branch namedv0.1.4, which does not exist, and the build fails withfail to download and extract the custom buildpack: Invalid status code: 404 Not Found. The.tar.gzform above is downloaded as is and stays pinned to the release.
Make sure config/database.yml reads DATABASE_URL in production — the section rails new generates targets a local socket with a password variable instead:
production:
primary: &primary_production
<<: *default
url: <%= ENV["DATABASE_URL"] %>Provision a PostgreSQL addon (scalingo --app <app> addons-add postgresql <plan>), push, and the app boots with RAILS_ENV, RAILS_LOG_TO_STDOUT and RAILS_SERVE_STATIC_FILES already set by the buildpack.
One database
The addon is a single database, but the production section rails new generates also declares a cache database for Solid Cache (config/cache.yml says database: cache, its schema is db/cache_schema.rb). The postdeploy db:migrate never loads that schema, so the cache table is never created. Keep the cache in the app's database:
- drop the
cacheentry from the production section ofconfig/database.yml, and thedatabase: cacheline fromconfig/cache.yml - create the table with a regular migration (
bin/rails g migration CreateSolidCacheEntries), copyingcreate_table "solid_cache_entries"and its indexes fromdb/cache_schema.rb - delete
db/cache_schema.rb
The same goes for any other extra database in that section (queue, cable).
Environment variables
Besides the three above, the code Trek generates reads:
| Variable | Used for |
|---|---|
DEFAULT_HOSTNAME | The app's host, for URLs built outside a request, including links in emails (config/initializers/host.rb) |
MAILER_FROM | The sender of the app's emails (ApplicationMailer) |
S3_BUCKET, S3_REGION, S3_ENDPOINT, S3_ACCESS_KEY_ID, S3_SECRET_ACCESS_KEY | The uploads' bucket (config/initializers/shrine.rb). Required in production: the app doesn't boot without them |
S3_BASE_URL | The public URL of the bucket's files, e.g. https://<bucket>.<region>.your-objectstorage.com. Required in production too |
APPSIGNAL_PUSH_API_KEY | Turns AppSignal on, unless the key is in the credentials (config/appsignal.rb) |
APPSIGNAL_APP_ENV | The AppSignal environment, e.g. qa. The gem reads this name, not APPSIGNAL_ENV |
RAILS_MAX_THREADS, GOOD_JOB_MAX_THREADS | Puma's threads and the jobs' threads, which also size the database pool (config/database.yml) |
Jobs run inside the web container (config.good_job.execution_mode = :async), so the generated Procfile has no worker to scale.
env-set doesn't restart the app: running containers keep their old values until scalingo --app <app> restart.
Domains
Add each domain to the app, then point it at the app with a CNAME:
scalingo --app <app> domains-add <domain>
# DNS: <domain> CNAME <app>.osc-fr1.scalingo.io.Scalingo issues the Let's Encrypt certificate once the CNAME resolves; until then the domain shows "Pending DNS".
Continuous deployment
Deploy from GitLab CI rather than linking the Scalingo app to the repository: the deploy then only runs once the pipeline's tests, lint and security jobs have passed. The generated .gitlab-ci.yml has a deploy stage, with commented-out jobs; replace them with one job per Scalingo app:
qa:
<<: *deploy_definition
only:
- main
environment:
name: qa
url: https://<domain>
script:
- curl -O https://cli-dl.scalingo.com/install && bash install
- scalingo login --api-token $SCALINGO_API_TOKEN
- tar --exclude=tmp --exclude=log --exclude=spec --exclude=.git --transform='s,^,master/,' -cvzf tmp/main.tar.gz .
- scalingo --app <app> deploy tmp/main.tar.gz $CI_COMMIT_SHORT_SHASCALINGO_API_TOKEN is a Scalingo API token, set as a masked CI/CD variable of the project or its group. A production job on the production branch follows the same pattern.
Before going live, check what Trek expects from the hosting: backups, a shared cache store, the scheduled purges (see Security and compliance).
Options
| Variable | Effect |
|---|---|
TREK_BUILDPACK_RUNTIME_NODE=1 | Ship Node.js in the image (only if the app runs JavaScript at runtime, e.g. execjs) |
TREK_BUILDPACK_PRUNE_SOURCEMAPS=1 | Also remove .map files from the compiled assets |
Migrating an existing app
Apps deployed with the stock .buildpacks (apt + ruby) migrate in three steps:
- delete
.buildpacks(re-runningbin/rails g trek:scalingodoes it) and check that.node-versionexists — apps generated withrails new -j esbuildhave it - set
BUILDPACK_URLas above, and mapDATABASE_URLif the app doesn't read it yet - deploy — the first build fills the caches, the next ones benefit from them
The buildpack's README details the cache layout and the Aptfile syntax.