This assumes you’re starting with an existing Kirby project and you don’t keep your content/
folder in version control.
Create a new “Laravel” app:
touch artisan
fly launch
rm artisan
Since Fly.io is a fancy platform-as-a-service and not an old school host where you just throw files and they stay there no matter what, you’ll need to take special care to make a place for Kirby’s content/
(where all your content is) and storage/
(where your user account data is) that lives outside of what code you deploy.
Create a folder called persistent
in the root of your project. Move both your content
and storage
folders into it.
Tell Kirby where to find where to find those folders. Adjust the values for content
and storage
in public/index.php
like so:
<?php
include __DIR__ . '/../kirby/bootstrap.php';
$kirby = new Kirby([
'roots' => [
'index' => __DIR__,
'base' => $base = dirname(__DIR__),
'content' => $base . '/persistent/content', # CHANGED
'site' => $base . '/site',
'storage' => $storage = $base . '/persistent/storage', # CHANGED
'accounts' => $storage . '/accounts',
'cache' => $storage . '/cache',
'sessions' => $storage . '/sessions',
]
]);
echo $kirby->render();
Create the volume. Make sure to use the same region as you selected for your app.
fly volumes create persistent --size 1
Mount the volume in your fly.toml
(at the end of the file):
# More stuff…
[[mounts]]
source = "persistent"
destination = "/var/www/html/persistent"
processes = ["app"]
.gitignore
and .dockerignore
.Be sure to add persistent/
as well as other stuff that Kirby already has in their default .gitignore
such as:
persistent/
/public/media
!/public/media/index.html
/site/cache/*
!/site/cache/index.html
/site/accounts/*
!/site/accounts/index.html
/site/sessions/*
!/site/sessions/index.html
/site/config/.license
.lock
/kirby
/vendor
entrypoint
scriptAdd these lines to the top of .fly/entrypoint.sh
to make it Kirby-friendly:
#!/usr/bin/env sh
# Disable Swoole to prevent `Cannot redeclare go()` error
phpdismod swoole
# More stuff…
Dockerfile
Remove this stuff:
artisan
or laravel
and other Laravel-related thingsnode_modules_go_brrr
stuff.npm run build
Here’s my entire Dockerfile
. If you figure out how to improve this (hopefully simplifying it even more), please let me know!
# syntax = docker/dockerfile:experimental
# Default to PHP 8.2, but we attempt to match
# the PHP version from the user (wherever `flyctl launch` is run)
# Valid version values are PHP 7.4+
ARG PHP_VERSION=8.2
ARG NODE_VERSION=18
FROM fideloper/fly-laravel:${PHP_VERSION} as base
# PHP_VERSION needs to be repeated here
# See https://docs.docker.com/engine/reference/builder/#understand-how-arg-and-from-interact
ARG PHP_VERSION
LABEL fly_launch_runtime="laravel"
# copy application code, skipping files based on .dockerignore
COPY . /var/www/html
RUN composer install --no-interaction --prefer-dist --optimize-autoloader \
&& mkdir -p storage/logs \
&& chown -R www-data:www-data /var/www/html \
&& cp .fly/entrypoint.sh /entrypoint \
&& chmod +x /entrypoint
# Multi-stage build: Build static assets
# This allows us to not include Node within the final container
FROM node:${NODE_VERSION}
RUN mkdir /app
RUN mkdir -p /app
WORKDIR /app
COPY . .
RUN npm i && npm run build;
# From our base container created above, we
# create our final image, adding in static
# assets that we generated above
FROM base
EXPOSE 8080
ENTRYPOINT ["/entrypoint"]
I had to adjust composer.json
(changing the required PHP version from <8.2.0
to <8.3.0
because Fly had 8.2.9
by default) then run composer update
.
composer.json
:
# More stuff…
"require": {
"php": ">=7.4.0 <8.3.0",
# More stuff…
For some reason, the default $_SERVER['SERVER_NAME']
doesn’t seem to want to play nice on Fly. Just set it manually. It’s cool to add an array to list multiple URLs here (for local dev + remote, for example) and Kirby should sort it out automatically.
Add your URL to site/config/config.php
:
<?php
return [
'url' => 'https://whatever.fly.dev',
# More stuff…
fly ssh console
# Edit site/config/config.php somehow
vim site/config/config.php
Add 'install' => true
to 'panel'
:
# More stuff…
'panel' => [
'install' => true,
],
# More stuff…
Go to the Panel URL and create a user. Once that’s done, remove what you added to site/config/config.php
.
.license
file to persistent storage.license
file to persistent/
storage:fly ssh console
cp site/config/.license persistent
chown www-data:www-data persistent/.license
Add the following stuff to your entrypoint.sh
right above chown -R www-data:www-data /var/www/html
# More stuff…
if [ -f /var/www/html/persistent/.license ]; then
cp /var/www/html/persistent/.license /var/www/html/site/config
fi
# More stuff…
www
and your default Fly URL to your base domain nameCopy the standard Nginx config from Fly’s Laravel Docker image and put it in the .fly/
folder (via). Add something like this at the top of the file:
.fly/default
:
server {
listen 8080;
listen [::]:8080;
server_name www.example.com;
rewrite ^(.*) https://example.com$1 permanent;
}
server {
listen 8080;
listen [::]:8080;
server_name example.fly.dev;
rewrite ^(.*) https://example.com$1 permanent;
}
# All the existing stuff:
server {
# More stuff…
In your Dockerfile
, add a line to copy that new file (right after && chmod +x /entrypoint \
)
&& chmod +x /entrypoint \ # Add trailing backslash
&& cp .fly/default /etc/nginx/sites-available/default # New
At this point, you should be good to go! Keep reading to learn how to automatically back up your content/
folder to S3-compatible storage every day.
Be sure to deploy using the command fly deploy --ha=false
so it doesn’t try to create multiple machines.
content/
to Backblaze B2 or other S3-compatible things(Hopefully not actually S3. Amazon sucks.)
Dockerfile
Put this stuff right above the COPY . /var/www/html
# Install cron
RUN apt-get install -y cron
# Install AWS CLI
RUN curl "https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip" -o "awscliv2.zip" && \
unzip awscliv2.zip && \
./aws/install
Set Fly secrets for these items with the appropriate values from your S3-compatible storage service of choice.
AWS_ACCESS_KEY_ID=your-access-key-id
AWS_SECRET_ACCESS_KEY=your-secret-access-key
I like to keep a .gitignore
‘d fly.env
file in the root of my project. Then you can just run the this command to update things and keep track of the current values:
fly secrets import < fly.env
entrypoint.sh
Put this stuff in your entrypoint.sh
right below the phpdismod swoole
line:
# Load cron configuration.
crontab /var/www/html/crontab
echo "Cron has been configured." >> /var/log/cron.log
# Start cron as a daemon.
cron
echo "Cron has been started." >> /var/log/cron.log
# For the sake of cron having access to AWS credentials
printf "AWS_ACCESS_KEY_ID=%s\n" $AWS_ACCESS_KEY_ID >> /etc/environment
printf "AWS_SECRET_ACCESS_KEY=%s\n" $AWS_SECRET_ACCESS_KEY >> /etc/environment
This starts up cron and copies over your Fly secrets in a place where cron can read them.
Create a scripts/backup.sh
file:
#!/bin/bash
# Send content/ folder to B2
/usr/local/bin/aws s3 cp \
/var/www/html/persistent/content/ s3://your-bucket/your-project/$(date +"%F")/ \
--recursive \
--endpoint=https://your-endpoint
Make sure the file is executable:
chmod +x scripts/backup.sh
This will create a new folder named the current date (yyyy-mm-dd
) in the bucket and folder of your choice filled with what was in the content/
folder that day. You should probably set up some lifecycle rules to only keep a certain number of days. A week’s worth or so is good for me.
Create a crontab
file in the root of your project that looks something like this.
# For debugging if needed. Runs every minute.
# */1 * * * * /var/www/html/scripts/backup.sh >> /var/log/cron.log 2>&1
# 5am Eastern US time every day
0 9 * * * /var/www/html/scripts/backup.sh
Once you sign up and configure a “check,” you’ll just need to add a bit of text at the end of that last line in your crontab
. It’ll look something like
0 9 * * * /var/www/html/scripts/backup.sh && curl -fsS -m 10 --retry 5 -o /dev/null https://hc-ping.com/something