# Dokku with Laravel cookbook

January 12, 2025


## Why Dokku rather than Forge or Ploi?

My reasons:
- 🔋 Zero downtime deploys baked-in
- 🔒 Each app is isolated by Docker
- 💸 No additional costs or security risks
- ♾️ Not limited to PHP (anything deploys)

In comparison, Forge supports zero downtime deploys by making you subscribe to [Envoyer](https://envoyer.io) (10$/mo) or reaching out for [other hacks](https://chrysanthos.xyz/article/zero-downtime-deployments-for-laravel). 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:

```shell
php artisan key:generate --show
```

Then copy the key and set it remotely:

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

```shell
dokku storage:ensure-directory appname-db
```

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

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

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

And set the required environment variable:

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

## Octane for max performance (Dockerfile)

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

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

```shell
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.



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

```shell
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
```

