---
url: /deploy.md
---
# Deploying to Scalingo

Trek apps deploy to [Scalingo](https://scalingo.com) with the dedicated [trek-buildpack](https://github.com/etaminstudio/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), skipping `apt-get update` and package resolution entirely when the `Aptfile` is unchanged
* installs Ruby with [rv](https://rv.dev) — precompiled, in seconds — using the version pinned in `.ruby-version`
* installs Node.js pinned by `.node-version` (generated by `rails new`), and the exact Yarn release pinned by `package.json`'s `packageManager` field — no unpinned defaults
* runs `yarn install` exactly once, with a persistent cache, before `assets: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:

```sh
bin/rails g trek:scalingo
```

Then configure the Scalingo app's environment, pinning the buildpack to a release through its archive:

```sh
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-set` prints every value it sets, so without `> /dev/null` the 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 `#tag` suffix: Scalingo reads `#ref` on `BUILDPACK_URL` as a **branch**, so
> `…/trek-buildpack#v0.1.4` asks for a branch named `v0.1.4`, which does not exist, and the build fails with
> `fail to download and extract the custom buildpack: Invalid status code: 404 Not Found`. The `.tar.gz` form 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:

```yml
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 `cache` entry from the production section of `config/database.yml`, and the `database: cache` line from `config/cache.yml`
* create the table with a regular migration (`bin/rails g migration CreateSolidCacheEntries`), copying `create_table "solid_cache_entries"` and its indexes from `db/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:

```sh
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:

```yaml
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_SHA
```

`SCALINGO_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](/security#what-trek-expects-from-the-hosting)).

## 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:

1. delete `.buildpacks` (re-running `bin/rails g trek:scalingo` does it) and check that `.node-version` exists — apps generated with `rails new -j esbuild` have it
2. set `BUILDPACK_URL` as above, and map `DATABASE_URL` if the app doesn't read it yet
3. deploy — the first build fills the caches, the next ones benefit from them

The buildpack's [README](https://github.com/etaminstudio/trek-buildpack#readme) details the cache layout and the `Aptfile` syntax.
