Skip to main content
This guide walks you through building and deploying a Laravel application with MariaDB to Magic Containers with GitHub Container Registry. You’ll need:
  • A GitHub account for source code and container registry
  • A bunny.net account with Magic Containers enabled

Create the Laravel app

Create a new Laravel project:
composer create-project laravel/laravel app-laravel
cd app-laravel
Update your .env to use MariaDB:
.env
DB_CONNECTION=mariadb
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=laravel
DB_USERNAME=laravel
DB_PASSWORD=laravel
Use 127.0.0.1 instead of localhost for DB_HOST. Magic Containers share a localhost network between containers, but PHP/PDO interprets localhost as a Unix socket connection which will fail. Using 127.0.0.1 forces a TCP connection.

Create the Nginx config

Create docker/nginx.conf:
docker/nginx.conf
server {
    listen 80;
    server_name _;
    root /var/www/html/public;

    add_header X-Frame-Options "SAMEORIGIN";
    add_header X-Content-Type-Options "nosniff";

    index index.php;
    charset utf-8;

    location / {
        try_files $uri $uri/ /index.php?$query_string;
    }

    location = /favicon.ico { access_log off; log_not_found off; }
    location = /robots.txt  { access_log off; log_not_found off; }

    error_page 404 /index.php;

    location ~ \.php$ {
        fastcgi_pass 127.0.0.1:9000;
        fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name;
        include fastcgi_params;
        fastcgi_hide_header X-Powered-By;
    }

    location ~ /\.(?!well-known).* {
        deny all;
    }
}

Create the supervisord config

The app container runs both PHP-FPM and Nginx using supervisord. Create docker/supervisord.conf:
docker/supervisord.conf
[supervisord]
nodaemon=true
logfile=/dev/stdout
logfile_maxbytes=0

[program:php-fpm]
command=php-fpm -F
stdout_logfile=/dev/stdout
stdout_logfile_maxbytes=0
stderr_logfile=/dev/stderr
stderr_logfile_maxbytes=0

[program:nginx]
command=nginx -g "daemon off;"
stdout_logfile=/dev/stdout
stdout_logfile_maxbytes=0
stderr_logfile=/dev/stderr
stderr_logfile_maxbytes=0

Create the entrypoint script

Create docker/entrypoint.sh:
docker/entrypoint.sh
#!/bin/sh
cd /var/www/html

touch .env

php artisan config:cache
php artisan route:cache
php artisan view:cache

echo "Waiting for database..."
until php artisan db:monitor --databases=mariadb > /dev/null 2>&1; do
    sleep 1
done
echo "Database is ready."

php artisan migrate --force

exec supervisord -c /etc/supervisord.conf
The entrypoint creates an empty .env file so Laravel reads configuration from the container’s environment variables instead of a file. Config caching happens at startup (not at build time) so it picks up the runtime environment. The script also waits for MariaDB to be ready before running migrations.

Create the Dockerfile

Dockerfile
FROM php:8.4-fpm-alpine

RUN apk add --no-cache nginx supervisor curl \
    && docker-php-ext-install pdo pdo_mysql

COPY --from=composer:2 /usr/bin/composer /usr/bin/composer

WORKDIR /var/www/html

COPY composer.json composer.lock ./
RUN composer install --no-dev --no-scripts --no-autoloader --prefer-dist

COPY . .

RUN composer dump-autoload --optimize

RUN chown -R www-data:www-data storage bootstrap/cache

COPY docker/nginx.conf /etc/nginx/http.d/default.conf
COPY docker/supervisord.conf /etc/supervisord.conf
COPY docker/entrypoint.sh /entrypoint.sh
RUN chmod +x /entrypoint.sh

EXPOSE 80

CMD ["/entrypoint.sh"]
Do not run php artisan config:cache in the Dockerfile. Caching config at build time bakes in the build environment’s values, which won’t match the runtime environment variables set in Magic Containers. The entrypoint script handles config caching at startup instead.
Create a .dockerignore to keep the .env file out of the image:
.dockerignore
.env
.env.example
.git
node_modules
vendor
storage/logs/*
tests
.github

Create the docker-compose file

Create docker-compose.yml for local development:
docker-compose.yml
services:
  app:
    build: .
    ports:
      - "8000:80"
    environment:
      - DB_CONNECTION=mariadb
      - DB_HOST=db
      - DB_PORT=3306
      - DB_DATABASE=laravel
      - DB_USERNAME=laravel
      - DB_PASSWORD=laravel
    depends_on:
      db:
        condition: service_healthy

  db:
    image: mariadb:11
    environment:
      MARIADB_DATABASE: laravel
      MARIADB_USER: laravel
      MARIADB_PASSWORD: laravel
      MARIADB_ROOT_PASSWORD: root
    volumes:
      - dbdata:/var/lib/mysql
    healthcheck:
      test: ["CMD", "healthcheck.sh", "--connect", "--innodb_initialized"]
      interval: 5s
      timeout: 5s
      retries: 5

volumes:
  dbdata:
The docker-compose.yml uses DB_HOST=db (the service name) for local development. In Magic Containers, the bunny.json sets DB_HOST=127.0.0.1 since containers share the same localhost network.

Run locally

docker compose up --build
Visit http://localhost:8000.

Generate an app key

Generate a key to use in production:
php artisan key:generate --show
Keep this value for the next step.

Build and push to GitHub Container Registry

Create .github/workflows/deploy.yml to automatically build and push on every commit to main:
.github/workflows/build.yml
name: Build and Push

on:
  push:
    branches: [main]

env:
  REGISTRY: ghcr.io
  IMAGE_NAME: ${{ github.repository }}

jobs:
  build-and-push:
    runs-on: ubuntu-latest
    permissions:
      contents: read
      packages: write

    steps:
      - uses: actions/checkout@v4

      - name: Log in to GitHub Container Registry
        uses: docker/login-action@v3
        with:
          registry: ${{ env.REGISTRY }}
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}

      - name: Build and push
        uses: docker/build-push-action@v5
        with:
          context: .
          push: true
          tags: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.sha }}

      - name: Update container image on Magic Containers
        uses: BunnyWay/actions/container-update-image@main
        with:
          app_id: ${{ vars.APP_ID }}
          api_key: ${{ secrets.BUNNYNET_API_KEY }}
          container: app
          image_tag: "${{ github.sha }}"
Push your code to trigger the workflow:
git init
git add .
git commit -m "Initial commit"
git remote add origin https://github.com/YOUR_USERNAME/app-laravel.git
git push -u origin main
If your package is private, set the visibility to Public in GitHub or configure Magic Containers with registry credentials.

Deploy to Magic Containers

1

Create a new app

In the bunny.net dashboard, go to Magic Containers and click Add App. Enter a name and select your deployment option.
2

Add a container

Click Add Container, then configure:
FieldValue
RegistryGitHub Container Registry
ImageYOUR_USERNAME/{imageName}
Taglatest for Docker CLI, or the commit SHA from your GitHub Actions workflow
3

Add an endpoint

Go to the Endpoints tab, click Add New Endpoint, and set the container port to 80.
4

Deploy

Click Add Container, then Next Step, and Confirm and Create.
For more details, see the quickstart guide. When configuring the app, add two containers:

App container

  • Image: ghcr.io/<your-username>/app-laravel:latest
  • Endpoint: port 80
  • Environment variables:
    • APP_ENV = production
    • APP_DEBUG = false
    • APP_KEY = the key from php artisan key:generate --show
    • DB_CONNECTION = mariadb
    • DB_HOST = 127.0.0.1
    • DB_PORT = 3306
    • DB_DATABASE = laravel
    • DB_USERNAME = laravel
    • DB_PASSWORD = laravel

Database container

  • Image: mariadb:11
  • Volume: /var/lib/mysql
  • Environment variables:
    • MARIADB_DATABASE = laravel
    • MARIADB_USER = laravel
    • MARIADB_PASSWORD = laravel
    • MARIADB_ROOT_PASSWORD = root

Test your app

curl https://mc-xxx.bunny.run
You can add a custom hostname from the Endpoints section in your app settings.

Continuous deployment

The workflow automatically deploys to Magic Containers on every push to main. Configure the following in your repository settings:
  • Variable APP_ID - your Magic Containers app ID
  • Secret BUNNYNET_API_KEY - your bunny.net API key

Key differences for Magic Containers

When deploying Laravel to Magic Containers, there are a few important things to keep in mind:
TopicWhat to doWhy
DB_HOSTUse 127.0.0.1, not localhostPHP/PDO treats localhost as a Unix socket. Magic Containers share a localhost network, so TCP via 127.0.0.1 is required.
Config cachingCache at startup, not build timephp artisan config:cache in the Dockerfile bakes in build-time values. Cache in the entrypoint so runtime env vars are used.
.env fileExclude from Docker imageAdd .env to .dockerignore and touch .env in the entrypoint. This ensures Laravel reads from container environment variables.
MigrationsRun at startupThe entrypoint waits for MariaDB, then runs php artisan migrate --force so the database is always up to date.
App keySet via environment variableGenerate with php artisan key:generate --show and set APP_KEY in the container environment.

Next steps

  1. Automate deploys with GitHub Actions
  2. Add a custom hostname
  3. Add a persistent volume