Running a site
Deploying
Put the site on a server with Kamal — the deploy files, secrets, storage, the first deploy, seeding the database, and what every deploy checks before it serves.
Nibble deploys with Kamal: one Docker image onto one server. There is no managed
database and no separate front end — SQLite and the uploads live on the server’s storage volume, and the image runs
everything. This guide takes Tidewater’s site from a laptop to https://tidewater.example.
After reading this guide, you will know:
- Which files a deploy needs, and how to generate them.
- Where the registry password and the credentials key come from.
- How to deploy the first time, and every time after.
- How to put content into a new, empty database.
- What a container checks before it serves, and what happens when a check fails.
1. What runs on the server
Inside the container: Puma behind Thruster, the Node process that renders pages on the server, and Solid Queue — for
scheduled publishing, emails and background work — inside Puma. SQLite files and uploaded files live on a Docker
volume mounted at /rails/storage, so they survive every deploy.
You need a server you can SSH into with Docker installable, a domain pointing at it, and an account with a container registry such as Docker Hub or GitHub’s.
2. The deploy files
If you did not ask for them at install, generate them now:
bin/rails nibble:install --only=deploy
It asks the site’s questions again, then the deploy’s:
| Question | Tidewater’s answer |
|---|---|
| Production domain | tidewater.example |
| Production server address | 203.0.113.10 |
| Container registry username | tidewater |
| SSH user on the servers | root |
and writes:
| File | What it is |
|---|---|
Dockerfile, .dockerignore, bin/docker-entrypoint |
how the image is built and started |
config/deploy.yml |
Kamal’s configuration, with Nibble’s settings merged in |
.kamal/secrets |
where Kamal finds each secret — Kamal’s own file |
.kamal/hooks/ |
Kamal’s sample hooks |
All of them are yours: edit them freely, and commit them.
2.1 config/deploy.yml
The parts Nibble fills in:
service: tidewater
image: tidewater/tidewater
servers:
web:
- 203.0.113.10
proxy:
ssl: true
hosts:
- tidewater.example
registry:
username: tidewater
password:
- KAMAL_REGISTRY_PASSWORD
env:
secret:
- RAILS_MASTER_KEY
clear:
SITE_URL: https://tidewater.example
SMTP_ADDRESS: smtp.postmarkapp.com
SMTP_PORT: 587
SOLID_QUEUE_IN_PUMA: true
volumes:
- tidewater-storage:/rails/storage
asset_path: /rails/public
Add the rest of the site’s settings under env.clear — see
Configuration. Tidewater’s also sets:
AWS_BUCKET_NAME: tidewater-uploads
AWS_REGION: eu-west-2
DB_SNAPSHOT_BUCKET: tidewater-backups
DB_SNAPSHOT_REGION: eu-west-2
Tip
Uploads do not need S3. Without AWS_BUCKET_NAME, they are stored on the server’s storage volume — fine for a
small site or a demo. Use a bucket once losing the server would mean losing images.
Important
A copy of the site that is public but is not the real one — a demo, a preview — should also set
NIBBLE_BLOCK_INDEXING: 1, so search engines do not index it.
Warning
Until you uncomment them, the first deploy fails for want of KAMAL_REGISTRY_PASSWORD and RAILS_MASTER_KEY.
Uncomment these two lines in .kamal/secrets:
KAMAL_REGISTRY_PASSWORD=$KAMAL_REGISTRY_PASSWORD
RAILS_MASTER_KEY=$(cat config/master.key)
The registry password is then read from your shell — set KAMAL_REGISTRY_PASSWORD to a registry access token, not
your account password — and the credentials key from config/master.key. The file holds no secrets itself, so it is
safe to commit. Kamal’s comments in the file show how to read them from a password manager instead.
Everything else sensitive goes in the encrypted credentials:
bin/rails credentials:edit
smtp:
username: …
password: …
aws:
access_key_id: …
secret_access_key: …
Caution
config/master.key is not in git, and nothing else can decrypt the credentials. Keep a copy somewhere safe — a
deploy from a new laptop needs it, and so does restoring a server.
2.3 A bucket for uploads, if you use one
The bucket does not need to be public: images are served through the application, which is what lets it crop and resize them. The control panel uploads straight from the browser, so the bucket needs a CORS rule for the site:
[
{
"AllowedOrigins": ["https://tidewater.example"],
"AllowedMethods": ["PUT"],
"AllowedHeaders": ["Content-Type", "Content-MD5", "Content-Disposition"],
"MaxAgeSeconds": 3600
}
]
3. The first deploy
bin/kamal setup
setup installs Docker on the server if it needs to, builds the image, pushes it, and starts the site behind
Kamal’s proxy, which fetches a TLS certificate for the domain.
Building the image runs bin/rails nibble:build, so a help article with bad frontmatter fails the build on your
machine rather than the deploy on the server.
3.1 Seeding the new database
A new server starts with an empty database. The schema is there, the help centre is there — it is files — but there are no pages, posts or people.
Create an administrator:
bin/kamal app exec -i 'bin/rails nibble:admin:create'
Then either write the content in the production control panel, or import the content package you keep in the repository:
bin/kamal app exec 'bin/rails nibble:content:import site/themes/tidewater/default_content'
and build the search index, so the help centre is searchable:
bin/kamal app exec 'bin/rails nibble:search:rebuild'
Warning
Importing is a one-time step, not part of a deploy. Running it again later would fight whatever has been written in
the control panel since. The default mode, create, only adds what is missing — so a second run is harmless, but
it is never needed.
Note
A content package carries each image’s details, not the image itself. Upload images again, or copy the storage volume or the bucket, if the imported pages use any.
4. Every deploy after
git push
bin/kamal deploy
Before the new container accepts a request, bin/docker-entrypoint runs bin/rails nibble:prepare, which:
- Refuses a theme built for a different theme API.
- Runs database migrations, then content migrations.
- Runs
nibble:check, which refuses a schema change that would strand stored content. - Indexes pages written as files for search, and builds the whole index when it is empty.
- Records the schema it is serving.
If anything refuses, the new container never becomes healthy. Kamal keeps the old one serving, and the container’s log says exactly why. A bad deploy is a non-event rather than an outage.
Important
Database migrations run before the check, as in any Rails deploy, so write them to work with the old release still serving.
5. Day to day
bin/kamal console # a Rails console on the server
bin/kamal shell # a shell in the container
bin/kamal logs # follow the logs
bin/kamal dbc # the database console
Health. /up answers 200 when the application has booted; Kamal’s proxy uses it.
CDNs. A proxying CDN in front of the site works with no rules to write. Asset URLs are versioned by content and
cached for a long time; HTML is sent Cache-Control: private, because caching a whole page at the edge could share
one visitor’s session with another. Nibble’s own page cache sits inside the application, where it knows who is
asking.
Backups. A nightly job copies the database and, with DB_SNAPSHOT_BUCKET set, uploads it. See
Backups.