Первый деплой с Kamal: от HTML-файла до сайта с HTTPS
Разворачиваем простую страницу на своём сервере: Nginx в Docker, образы в GHCR, домен и обновления через Kamal.
Чтобы разобраться с деплоем, достаточно одной HTML-страницы. Не нужны фреймворк, база данных или сборщик: так проще увидеть, за что отвечает каждый инструмент.
Сделаем страницу, упакуем её в Docker и опубликуем на своём сервере через Kamal. В результате сайт будет открываться по домену с HTTPS, а для следующего обновления понадобится одна команда.
Как это работает
Nginx внутри контейнера раздаёт HTML. Docker-образ хранится в GitHub Container Registry, или GHCR. Kamal подключается к серверу по SSH, доставляет образ и переключает на него трафик через kamal-proxy.
Ноутбук → сборка образа → GHCR → контейнер на сервере
↑
Браузер → HTTPS → kamal-proxy ──────────┘
TLS завершается на прокси. Между прокси и контейнером используется HTTP внутри Docker-сети, поэтому Nginx в приложении слушает обычный порт 8080.
Что подготовить
На компьютере нужны Git, Ruby, Docker с Buildx и GitHub CLI. Для локальной проверки Docker должен быть запущен. Здесь использую Kamal 2.11.0 — ту же версию, с которой проверена конфигурация этого блога:
gem install kamal --version 2.11.0 --no-document
kamal version
docker version
docker buildx version
Ещё понадобятся домен, аккаунт GitHub и сервер. Пример рассчитан на чистый Ubuntu 24.04 с архитектурой amd64 и первоначальным SSH-доступом к root по ключу.
Во всех примерах замените example.com своим доменом, 203.0.113.10 — IP сервера, а your-github-user — своим GitHub username в нижнем регистре. Это демонстрационные значения, а не адреса для реального деплоя.
1. Создаём страницу
Начнём с каталога проекта:
mkdir hello-kamal
cd hello-kamal
git init -b main
mkdir -p public config .kamal
Создайте public/index.html:
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Hello, Kamal</title>
<style>
body {
max-width: 640px;
margin: 15vh auto;
padding: 24px;
background: #141615;
color: #e7e8e3;
font: 18px/1.6 system-ui, sans-serif;
}
h1 { color: #b8d99b; }
</style>
</head>
<body>
<h1>Hello, Kamal.</h1>
<p>A single HTML file, running on my own server.</p>
</body>
</html>
2. Упаковываем сайт в Docker
Создайте config/nginx.conf:
server {
listen 8080;
server_name _;
root /usr/share/nginx/html;
index index.html;
absolute_redirect off;
location = /up {
access_log off;
default_type text/plain;
return 200 "ok\n";
}
location / {
try_files $uri $uri/ =404;
}
}
/up — адрес проверки готовности: Kamal проверит его перед переключением трафика. absolute_redirect off оставляет редиректы относительными, чтобы за внешним HTTPS-прокси в них не попал внутренний порт контейнера. Подробности есть в документации Nginx.
В корне проекта создайте Dockerfile:
FROM nginxinc/nginx-unprivileged:stable-alpine
LABEL org.opencontainers.image.source="https://github.com/your-github-user/hello-kamal"
COPY config/nginx.conf /etc/nginx/conf.d/default.conf
COPY public/ /usr/share/nginx/html/
EXPOSE 8080
Этот образ запускает Nginx без root. Метка org.opencontainers.image.source указывает, где лежат исходники.
В .dockerignore добавьте:
.git
.kamal
.env
.env.*
А в .gitignore:
.env
.env.*
.DS_Store
Проверим контейнер локально:
docker build -t hello-kamal .
docker run --rm -p 8080:8080 hello-kamal
Откройте http://localhost:8080. В другом терминале выполните:
curl --fail http://localhost:8080/up
Ожидаемый ответ — ok. Остановить контейнер можно через Ctrl+C в первом терминале.
3. Подготавливаем сервер и домен
Если на сервере уже есть Docker и пользователь для деплоя с доступом к нему, пропустите установку. Для чистого сервера подключитесь как root:
ssh root@203.0.113.10
Следующие команды выполняются на сервере. Они создают пользователя deploy и копируют ему ключи, с которыми уже разрешён вход в root:
apt-get update
apt-get install -y docker.io curl
systemctl enable --now docker
adduser --disabled-password --gecos "" deploy
install -d -m 700 -o deploy -g deploy /home/deploy/.ssh
cp /root/.ssh/authorized_keys /home/deploy/.ssh/authorized_keys
chown deploy:deploy /home/deploy/.ssh/authorized_keys
chmod 600 /home/deploy/.ssh/authorized_keys
usermod -aG docker deploy
exit
Доступ к Docker фактически даёт административные возможности на сервере; используйте для этого собственный доверенный ключ. После добавления в группу важно открыть новое SSH-соединение. Проверьте с компьютера:
ssh deploy@203.0.113.10 'docker version --format "{{.Server.Version}}"'
У провайдера DNS добавьте A-запись @ → 203.0.113.10. Уберите конфликтующую парковку или URL Redirect для корня домена. Если есть AAAA-запись, она должна вести на работающий IPv6 этого же сервера; иначе удалите её. Порты 22, 80 и 443 должны быть доступны в firewall сервера и в панели хостинга.
Перед деплоем проверьте адрес:
dig +short A example.com
Он должен совпадать с IP сервера. Для автоматического HTTPS домен должен вести на сервер, а порт 443 — быть доступен снаружи. Эти требования описаны в настройке прокси Kamal.
4. Подключаем GHCR
Дальнейшие команды выполняются в каталоге проекта на компьютере. Войдите в GitHub CLI и добавьте право публикации образов:
gh auth login --hostname github.com
gh auth refresh --hostname github.com --scopes write:packages
gh auth token --hostname github.com | docker login ghcr.io --username your-github-user --password-stdin
Подтвердите вход в браузере. Последняя команда передаёт токен прямо в Docker, не печатая его в терминал.
В .kamal/secrets запишите только команду получения токена:
KAMAL_REGISTRY_PASSWORD=$(gh auth token --hostname github.com)
Такой файл можно хранить в Git: самого секрета в нём нет. Не заменяйте команду буквальным значением токена в коммитах. Kamal поддерживает получение секретов через команды; это описано в документации переменных окружения.
Для отдельного токена GHCR используйте Personal Access Token classic с нужными правами на packages. В GitHub Actions вместо личного токена применяется встроенный GITHUB_TOKEN. Оба варианта разобраны в руководстве GitHub по GHCR.
5. Описываем деплой
Создайте config/deploy.yml:
service: hello-kamal
image: your-github-user/hello-kamal
servers:
web:
- 203.0.113.10
proxy:
ssl: true
host: example.com
app_port: 8080
healthcheck:
path: /up
registry:
server: ghcr.io
username: your-github-user
password:
- KAMAL_REGISTRY_PASSWORD
builder:
arch: amd64
ssh:
user: deploy
app_port должен совпадать с listen в Nginx. image задаёт имя образа внутри registry, а service — имя приложения для Kamal. Для сервера на ARM замените amd64 на arm64.
Этот вариант собирает образ на компьютере. Если Docker daemon есть только на сервере, замените блок builder на:
builder:
arch: amd64
remote: ssh://deploy@203.0.113.10
local: false
При удалённой сборке локально всё равно нужны Docker CLI и Buildx. Опции описаны в документации сборщика Kamal.
6. Выполняем первый деплой
Сначала сохраните файлы в Git и создайте репозиторий на GitHub:
git add .
git commit -m "Add a single-page site and Kamal deployment"
gh repo create hello-kamal --private --source=. --remote=origin --push
Kamal по умолчанию собирает локальный Git checkout, поэтому незакоммиченные изменения в образ не попадут.
На подготовленном сервере для первого запуска приложения выполните:
kamal setup
Если Docker и совместимый kamal-proxy уже работают, например для другого приложения, можно сразу использовать kamal deploy. Не запускайте рядом второй reverse proxy на тех же портах.
После успешного завершения проверьте:
curl --fail --location --head https://example.com
В браузере должна открыться наша страница по HTTPS. Сертификатом занимается kamal-proxy, отдельная установка Certbot для этого примера не нужна.
7. Обновляем сайт и откатываем изменения
Измените текст в public/index.html, затем выполните:
git add public/index.html
git commit -m "Update homepage copy"
git push
kamal deploy
Kamal доставит новый образ и проверит /up перед переключением трафика. Для диагностики и просмотра сохранённых контейнеров:
kamal app logs
kamal app containers -q
Чтобы вернуться к предыдущему релизу, возьмите его версию из имени старого контейнера и выполните kamal rollback VERSION. Замените VERSION реальным идентификатором. Откат требует, чтобы предыдущий контейнер или образ ещё был доступен; детали — в руководстве по rollback.
Если сайт не открывается
Сначала разделите DNS и работу сервера. Сравните обычный запрос с запросом к правильному IP:
curl --head --location --max-time 15 https://example.com
curl --head --location --max-time 15 --resolve example.com:443:203.0.113.10 https://example.com
Если второй работает, а первый — нет, проверьте, какой адрес возвращает DNS. После смены A-записи старый IP может оставаться в кеше роутера или провайдера до истечения TTL. Очистка кеша браузера не обновляет DNS роутера.
Если не работает и запрос по IP с --resolve, смотрите логи приложения, доступность портов и настройки домена. Не добавляйте -k к проверке HTTPS: иначе можно пропустить ошибку сертификата.
Теперь для публикации новой версии достаточно коммита и kamal deploy. Следующий шаг — перенести эту команду в GitHub Actions и запускать её после успешных проверок при слиянии в main.