Dokku with Laravel cookbook

Why Dokku rather than Forge or Ploi?

My reasons:

In comparison, Forge supports zero downtime deploys by making you subscribe to Envoyer (10$/mo) or reaching out for other hacks. Even then, such setup would be limited to PHP-FPM, making it impossible to run Octane or other languages/runtimes.

Setting an encryption key

Run this local command to receive a new encryption key:

php artisan key:generate --show

Then copy the key and set it remotely:

dokku config:set appname APP_KEY="YOUR_KEY"

SQLite as your database

Dokku stores persistent data in a “persistent” directory, so let’s create one:

dokku storage:ensure-directory appname-db

Then we can enter it to make a new production-ready WAL-mode database:

cd /var/lib/dokku/data/storage/appname-db
sqlite3 database.sqlite "
  PRAGMA journal_mode = WAL;
  PRAGMA auto_vacuum = INCREMENTAL;
  PRAGMA page_size = 4096;
  PRAGMA wal_autocheckpoint = 1000;
  VACUUM;
"

Finally, let’s mount our directory to the app itself:

dokku storage:mount appname /var/lib/dokku/data/storage/appname-db:/app/database/sqlite

And set the required environment variable:

dokku config:set appname DB_DATABASE=/app/database/sqlite/database.sqlite

Octane for max performance (Dockerfile)

### Stage 1: Asset Build
FROM node:20 AS assets-builder

WORKDIR /app
COPY package.json pnpm-lock.yaml ./
RUN corepack enable
RUN --mount=type=cache,id=pnpm,target=/root/.local/share/pnpm/store pnpm install --frozen-lockfile
COPY . .
RUN pnpm build

### Stage 2: Laravel Build
FROM composer:2 AS dependencies

WORKDIR /app
COPY . .
RUN composer install --no-dev --optimize-autoloader --no-interaction

### Stage 3: Final Image
FROM dunglas/frankenphp

COPY --from=composer /usr/bin/composer /usr/bin/composer
RUN install-php-extensions pcntl zip

WORKDIR /app
COPY . .
COPY --from=dependencies /app/vendor /app/vendor
COPY --from=assets-builder /app/public/build /app/public/build

EXPOSE 80
CMD ["php", "artisan", "octane:start", "--server=frankenphp", "--host=0.0.0.0", "--port=80", "--admin-port=2019"]

Queues

Procfile lets you define multiple processes for your app. Here’s mine:

web: php artisan octane:start --server=frankenphp --host=0.0.0.0 --port=80 --admin-port=2019
release: php artisan migrate --force
queue: php artisan queue:work

But, any new process won’t be activated until we scale it up:

dokku ps:scale appname queue=1

Server and provisioning

I prefer to use Debian 12 as it does not come with any extra bloat such as Snap on Ubuntu and thus uses minimum resources. It’s been proven to be super stable and run for years without reboots.

apt install unattended-upgrades -y

Disable password login for SSH:

sed -i -E 's/#?PasswordAuthentication (yes|no)/PasswordAuthentication no/' /etc/ssh/sshd_config
systemctl reload ssh

(assuming you have your SSH keys in authorized_keys)

Optimizations

You will get a much better resource utilization by enabling keepalive:

dokku nginx:set --global proxy-keepalive 64

Without it, there will consistently be a new TCP connection opened between Nginx and the Docker container, eventually making it hit the machine’s Linux configuration limit. Setting a keepalive to your Nginx proxy configuration prevents it.

Troubleshooting

Reset port configuration to a default state:

dokku ports:clear appname
dokku ports:set appname http:80:80
dokku ports:set appname https:443:80
dokku ps:restart appname