Deploy a WordPress + Laravel Monorepo
Containerize WordPress and Laravel in a monorepo and deploy to DigitalOcean.
Introduction
30 Second Summary
Many real-world web projects need more than one application living on the same domain. A marketing site at the root, a customer portal at a subpath, and shared infrastructure connecting them all.
In this project, you will build a monorepo containing a Dockerized WordPress CMS and a Laravel web application, then deploy both to DigitalOcean App Platform behind a single domain with path-based routing.
What You'll Build
A production-ready monorepo with WordPress served at the root path (/) and Laravel served at /portal, orchestrated locally with Docker Compose and deployed to DigitalOcean App Platform using a YAML app spec with managed MySQL.
By the end of this project, you'll have:
- A GitHub monorepo with WordPress and Laravel in separate subdirectories, each with its own Dockerfile.
- A Docker Compose setup for local development with a shared MySQL container.
- A live DigitalOcean App Platform deployment with path-based ingress routing and a managed MySQL database serving both applications on a single domain.
- Secret Mission: Add a Laravel health check endpoint and configure App Platform autoscaling with request-based metrics.
Are there any prerequisites?
You need a GitHub account, a DigitalOcean account, and Docker Desktop installed on Windows 11. Basic familiarity with Docker, Laravel, and WordPress is helpful but not required.
Set Up the Monorepo and Docker Environment
Before writing any Dockerfiles or deployment specs, you need a solid project structure on your machine.
A monorepo keeps all your related services in a single repository. In this step, you will create the folder structure, scaffold a fresh Laravel application using Docker, and prepare environment file templates for both apps.
In this step, get ready to:
- Create the monorepo folder structure with separate directories for WordPress, Laravel, and the deployment spec.
- Scaffold a fresh Laravel project using a Docker container.
- Configure database connection placeholders and push to GitHub.
Create the monorepo folder structure
Your monorepo needs three top-level directories: one for WordPress, one for Laravel, and one for the DigitalOcean App Platform deployment spec.
- In VS Code, open the integrated terminal by pressing Ctrl+` (Windows) or Cmd+` (macOS).
- Move to your Desktop by running this command:
cd ~/Desktop
- Create your project folder and move into it by running these commands:
mkdir wp-laravel-monorepo
cd wp-laravel-monorepo
- Create the three subdirectories by running:
mkdir wordpress laravel .do
What are these directories for?
The wordpress/ directory will hold the WordPress Dockerfile. WordPress itself comes from its official Docker image, so no source files go here.
The laravel/ directory will contain the full Laravel application source code and its own Dockerfile.
The .do/ directory is where DigitalOcean App Platform looks for its deployment spec (app.yaml).
- Add a .gitkeep file to the directories that start empty, so Git tracks them:
touch wordpress/.gitkeep .do/.gitkeep
- Initialize a Git repository and create your first commit by running:
git init
git add .
git commit -m "Initial monorepo structure"
- Confirm the directories exist by running:
ls
You should see wordpress and laravel in the output. The .do folder is hidden by default since its name starts with a dot.
Not seeing all three directories?
Run ls -a to show hidden files and directories. You should see .do listed alongside the others.
If you see a Git error about empty paths, make sure you ran the touch command to create the .gitkeep files first.
Help me troubleshoot my monorepo folder setup.
Scaffold the Laravel application with Docker
Instead of installing PHP and Composer on your local machine, you will use a temporary Docker container to scaffold Laravel. The container runs, creates the project files, and then removes itself.
Why use Docker for scaffolding?
Running composer create-project inside a Docker container means you do not need PHP or Composer installed locally. The composer:2 image provides everything needed.
The --rm flag tells Docker to automatically delete the container once the command finishes. Your Laravel files stay on your machine via the volume mount.
- Scaffold a fresh Laravel project inside the laravel/ directory by running the command for your platform:
macOS
docker run --rm -v "$(pwd)/laravel:/app" -w /app composer:2 create-project laravel/laravel .
Windows
docker run --rm -v "${PWD}/laravel:/app" -w /app composer:2 create-project laravel/laravel .
What does this command do?
- docker run --rm starts a temporary container that deletes itself when done.
- -v mounts your local laravel/ directory into the container at /app.
- -w /app sets the working directory inside the container.
- composer:2 create-project laravel/laravel . uses the Composer 2 image to create a new Laravel project in the current directory.
This command takes a minute or two while it downloads Laravel and its dependencies. You will see Composer output scrolling in the terminal.
- Verify the Laravel files exist by running:
ls laravel/
You should see standard Laravel directories like app, config, database, public, and routes in the output.
Seeing a directory not empty error?
This means the laravel/ directory already has files in it. Remove any leftover files and try again:
rm -rf laravel/*
rm -rf laravel/.*
Then re-run the docker run command above.
Help me fix the directory not empty error when scaffolding Laravel with Docker.
Create environment file templates and push to GitHub
Environment files (.env.example) document the database variables each app needs. Other developers and your deployment platform can see which values to fill in.
- Create the WordPress environment template by running:
cat > wordpress/.env.example << 'EOF'
WORDPRESS_DB_HOST=
WORDPRESS_DB_USER=
WORDPRESS_DB_PASSWORD=
WORDPRESS_DB_NAME=wordpress
EOF
What does this file do?
This .env.example file documents the four database variables WordPress needs. The host, user, and password are left blank because they differ between local development and production on DigitalOcean.
The database name is pre-set to wordpress as a sensible default.
Laravel already has a .env.example from the scaffold, but it defaults to SQLite. You need to update it for MySQL.
- In VS Code, open laravel/.env.example from the Explorer sidebar.
- Find the database section that looks like this:
DB_CONNECTION=sqlite
# DB_HOST=127.0.0.1
# DB_PORT=3306
# DB_DATABASE=laravel
# DB_USERNAME=root
# DB_PASSWORD=
- Replace that entire section with the following MySQL configuration:
DB_CONNECTION=mysql
DB_HOST=
DB_PORT=3306
DB_DATABASE=laravel
DB_USERNAME=
DB_PASSWORD=
Why leave host, username, and password blank?
These values change depending on where you run the app. Locally, they point to your Docker Compose MySQL container. In production on DigitalOcean, they use the managed database credentials.
Leaving them blank in the template makes the intent clear without leaking environment-specific values.
- Save laravel/.env.example.
- Remove the .gitkeep from wordpress/ since it now has the .env.example file:
rm wordpress/.gitkeep
- Stage and commit all new files by running:
git add .
git commit -m "Add Laravel scaffold and env templates"
Now push this repository to GitHub so DigitalOcean App Platform can access it later.
- Go to github.com/new in your browser.
- Enter wp-laravel-monorepo as the repository name.
- Leave it as a Public repository.
- Do NOT check any boxes for README, .gitignore, or license (your repo already has files).
- Click Create repository.
- Back in your terminal, connect the local repo to GitHub and push by running:
git remote add origin https://github.com/[[GITHUB_USERNAME="your-github-username"]]/wp-laravel-monorepo.git
git branch -M main
git push -u origin main
You should see Git push your commits successfully with output ending in a reference to main.
Push failed with authentication error?
GitHub no longer accepts passwords for Git operations. If prompted for credentials, you need a personal access token or SSH key configured.
Visit your GitHub Settings, then Developer settings, then Personal access tokens to generate a token. Use it in place of a password when Git prompts you.
Help me fix a GitHub authentication error when pushing.
Your monorepo is structured and live on GitHub. Next up, you will write Dockerfiles for both WordPress and Laravel to turn them into deployable containers.
Write Dockerfiles for WordPress and Laravel
With your monorepo structure in place, both applications need a way to describe how they run inside a container.
A Dockerfile is a recipe that tells Docker exactly what base image to start from, what dependencies to install, and how to configure the application. In this step, you will write a Dockerfile for each app and build them into images you can run locally.
In this step, get ready to:
- Write a Dockerfile for WordPress using the official WordPress image.
- Write a Dockerfile for Laravel with PHP extensions, Composer, and Apache configuration.
- Build and test both images locally to confirm they start correctly.
Write the WordPress Dockerfile
WordPress publishes an official Docker image that bundles Apache, PHP, and the WordPress source code. Your Dockerfile only needs to declare which base image to use and set the database connection environment variables.
- In VS Code, create a new file called Dockerfile inside your wordpress/ directory.
- Add the following content to wordpress/Dockerfile:
FROM wordpress:6.9.4-php8.3-apache
ENV WORDPRESS_DB_HOST=db
ENV WORDPRESS_DB_USER=wordpress
ENV WORDPRESS_DB_PASSWORD=wordpress
ENV WORDPRESS_DB_NAME=wordpress
EXPOSE 80
What does this Dockerfile do?
- FROM wordpress:6.9.4-php8.3-apache starts from the official WordPress image with Apache and PHP 8.3 pre-installed.
- The four ENV lines set default database credentials so the container knows how to connect to MySQL.
- EXPOSE 80 declares that the container listens on port 80 for HTTP traffic.
- Save wordpress/Dockerfile.
Why is this Dockerfile so short?
The official WordPress image already contains Apache, PHP, and the full WordPress source. You only need to layer on your configuration. When you deploy to DigitalOcean later, the environment variables will be overridden with real database credentials.
Write the Laravel Dockerfile
Laravel needs more setup than WordPress because you are building from a plain PHP image. You need to install PHP extensions, pull in Composer for dependency management, copy your app source, and configure Apache to serve from Laravel's public/ directory.
- Create a new file called Dockerfile inside your laravel/ directory.
- Add the following content to laravel/Dockerfile:
FROM php:8.4-apache
RUN apt-get update && apt-get install -y \
libpng-dev \
libjpeg-dev \
libfreetype6-dev \
libzip-dev \
libxml2-dev \
libcurl4-openssl-dev \
unzip \
git \
&& docker-php-ext-configure gd --with-freetype --with-jpeg \
&& docker-php-ext-install pdo_mysql mbstring xml curl zip bcmath gd \
&& a2enmod rewrite \
&& rm -rf /var/lib/apt/lists/*
COPY --from=composer:2 /usr/bin/composer /usr/bin/composer
WORKDIR /var/www/html
COPY . /var/www/html
RUN composer install --no-dev --optimize-autoloader --no-interaction
RUN chown -R www-data:www-data /var/www/html/storage /var/www/html/bootstrap/cache
ENV APACHE_DOCUMENT_ROOT=/var/www/html/public
RUN sed -ri -e 's!/var/www/html!${APACHE_DOCUMENT_ROOT}!g' /etc/apache2/sites-available/*.conf
RUN sed -ri -e 's!/var/www/!${APACHE_DOCUMENT_ROOT}!g' /etc/apache2/apache2.conf /etc/apache2/conf-available/*.conf
RUN echo '<Directory /var/www/html/public>\n\
AllowOverride All\n\
Require all granted\n\
</Directory>' > /etc/apache2/conf-available/laravel.conf \
&& a2enconf laravel
EXPOSE 80
What does this Dockerfile do?
- FROM php:8.4-apache uses the official PHP 8.4 image with Apache pre-installed.
- The RUN apt-get block installs system libraries needed to compile PHP extensions like GD (image processing) and ZIP.
- docker-php-ext-install adds the PHP extensions Laravel requires: pdo_mysql, mbstring, xml, curl, zip, bcmath, and gd.
- COPY --from=composer:2 pulls the Composer binary from a separate image so you do not need to install it manually.
- The sed commands and the laravel.conf block redirect Apache's document root to /var/www/html/public where Laravel's index.php lives.
- Save laravel/Dockerfile.
Seeing a syntax error in the Dockerfile?
Make sure the backslash characters (\) at the end of each line in the RUN block do not have trailing spaces after them. A space after \ breaks the line continuation.
help me fix a syntax error in my Laravel Dockerfile
✔️ Awesome, I've got everything!
Great. Double check you have saved both Dockerfiles before moving on.
ⓧ I'd like to double check the full code
Here is the complete wordpress/Dockerfile:
FROM wordpress:6.9.4-php8.3-apache
ENV WORDPRESS_DB_HOST=db
ENV WORDPRESS_DB_USER=wordpress
ENV WORDPRESS_DB_PASSWORD=wordpress
ENV WORDPRESS_DB_NAME=wordpress
EXPOSE 80
And here is the complete laravel/Dockerfile:
FROM php:8.4-apache
RUN apt-get update && apt-get install -y \
libpng-dev \
libjpeg-dev \
libfreetype6-dev \
libzip-dev \
libxml2-dev \
libcurl4-openssl-dev \
unzip \
git \
&& docker-php-ext-configure gd --with-freetype --with-jpeg \
&& docker-php-ext-install pdo_mysql mbstring xml curl zip bcmath gd \
&& a2enmod rewrite \
&& rm -rf /var/lib/apt/lists/*
COPY --from=composer:2 /usr/bin/composer /usr/bin/composer
WORKDIR /var/www/html
COPY . /var/www/html
RUN composer install --no-dev --optimize-autoloader --no-interaction
RUN chown -R www-data:www-data /var/www/html/storage /var/www/html/bootstrap/cache
ENV APACHE_DOCUMENT_ROOT=/var/www/html/public
RUN sed -ri -e 's!/var/www/html!${APACHE_DOCUMENT_ROOT}!g' /etc/apache2/sites-available/*.conf
RUN sed -ri -e 's!/var/www/!${APACHE_DOCUMENT_ROOT}!g' /etc/apache2/apache2.conf /etc/apache2/conf-available/*.conf
RUN echo '<Directory /var/www/html/public>\n\
AllowOverride All\n\
Require all granted\n\
</Directory>' > /etc/apache2/conf-available/laravel.conf \
&& a2enconf laravel
EXPOSE 80
Build and verify both images
Now that both Dockerfiles are written, you will build them into images and run each one to confirm they start correctly.
- Build the WordPress image by running this command from your monorepo root:
docker build -t wordpress-local ./wordpress
What does this command do?
- docker build reads the Dockerfile in the specified directory and creates an image.
- -t wordpress-local tags the resulting image with a human-readable name so you can reference it easily.
- ./wordpress points Docker to the build context directory containing the Dockerfile.
- Build the Laravel image by running:
docker build -t laravel-local ./laravel
The Laravel build takes longer because it installs system packages and runs Composer.
- Once both builds complete, start the WordPress container to verify it works:
docker run -d -p 8080:80 --name wp-test wordpress-local
- Open your browser and navigate to http://localhost:8080.
- You should see the WordPress installation wizard asking you to select a language.
Why does WordPress show an error about the database?
The WordPress container is trying to connect to a MySQL host called db which does not exist yet. You may see a database connection error instead of the install wizard. That is expected at this stage. The database container comes in the next step when you set up Docker Compose.
- Stop and remove the WordPress test container:
docker stop wp-test && docker rm wp-test
- Now start the Laravel container to verify it works:
docker run -d -p 8081:80 --name laravel-test laravel-local
- Open your browser and navigate to http://localhost:8081.
- You should see the Laravel welcome page with the Laravel logo and version information.
- Stop and remove the Laravel test container:
docker stop laravel-test && docker rm laravel-test
Don't see the Laravel welcome page?
- Check that the build completed without errors. Scroll up in your terminal output and look for any red error lines.
- Make sure no other service is using port 8081 on your machine. Run docker ps to check for running containers.
- Verify that your laravel/Dockerfile starts with FROM php:8.4-apache and not a different base image.
help me debug why my Laravel container is not showing the welcome page
Both images build and run successfully. Next up, you will wire them together with a shared MySQL database using Docker Compose.
Create Docker Compose for Local Development
You have two working Docker images. WordPress starts in isolation and Laravel starts in isolation. But a real application needs these services to talk to each other through a shared database.
Docker Compose lets you define all your services in a single YAML file and spin them up together with one command. In this step, you will write a docker-compose.yml that orchestrates MySQL, WordPress, and Laravel as a connected local development environment.
In this step, get ready to:
- Write a Docker Compose file that defines MySQL, WordPress, and Laravel services with shared networking.
- Run all three containers together and verify they work.
- Confirm both apps connect to the same MySQL instance.
Write the Docker Compose configuration
Both WordPress and Laravel need their own database, but you only want one MySQL server. An initialization script can create the second database automatically when the MySQL container starts for the first time.
- In VS Code, create a new file called init-db.sql at the root of your monorepo (the same level as your wordpress/ and laravel/ folders).
- Paste the following SQL into init-db.sql:
CREATE DATABASE IF NOT EXISTS laravel;
GRANT ALL PRIVILEGES ON laravel.* TO 'appuser'@'%';
FLUSH PRIVILEGES;
What does this SQL do?
The MySQL Docker image runs any .sql files it finds in /docker-entrypoint-initdb.d/ the first time it starts. This script creates a laravel database alongside the default wordpress database.
It also grants full privileges on the laravel database to appuser, so both apps can authenticate with the same credentials.
- Save init-db.sql.
Now you will create the main Docker Compose file that ties everything together.
- Create a new file called docker-compose.yml at the root of your monorepo (same level as init-db.sql).
- Paste the following configuration into docker-compose.yml:
services:
mysql:
image: mysql:8.0
environment:
MYSQL_ROOT_PASSWORD: rootpassword
MYSQL_DATABASE: wordpress
MYSQL_USER: appuser
MYSQL_PASSWORD: apppassword
ports:
- "3306:3306"
volumes:
- mysql_data:/var/lib/mysql
- ./init-db.sql:/docker-entrypoint-initdb.d/init-db.sql
wordpress:
build:
context: ./wordpress
dockerfile: Dockerfile
ports:
- "8080:80"
environment:
WORDPRESS_DB_HOST: mysql
WORDPRESS_DB_USER: appuser
WORDPRESS_DB_PASSWORD: apppassword
WORDPRESS_DB_NAME: wordpress
depends_on:
- mysql
laravel:
build:
context: ./laravel
dockerfile: Dockerfile
ports:
- "8081:80"
environment:
APP_KEY: base64:generated-key-here
APP_ENV: local
APP_DEBUG: "true"
DB_CONNECTION: mysql
DB_HOST: mysql
DB_PORT: "3306"
DB_DATABASE: laravel
DB_USERNAME: appuser
DB_PASSWORD: apppassword
depends_on:
- mysql
volumes:
mysql_data:
What does this configuration do?
- The mysql service runs a mysql:8.0 container, creates a default wordpress database, and mounts your init-db.sql to create the laravel database on first boot.
- The wordpress service builds from your wordpress/Dockerfile and maps port 8080 on your machine to port 80 inside the container.
- The laravel service builds from your laravel/Dockerfile and maps port 8081 on your machine to port 80 inside the container.
- Both app services use depends_on: mysql so Docker starts the database first.
- The mysql_data named volume persists your database between container restarts.
- Save docker-compose.yml.
- Confirm both files exist by running:
ls
You should see docker-compose.yml and init-db.sql listed alongside your wordpress/, laravel/, and .do/ directories.
Don't see the files?
Make sure you created both files at the monorepo root, not inside the wordpress/ or laravel/ subdirectories. Your terminal should be in the same folder that contains those subdirectories.
Help me find where my files ended up.
✔️ Awesome, I've got everything!
Great. Make sure both files are saved before moving on.
ⓧ I'd like to double check the full code
Here is init-db.sql:
CREATE DATABASE IF NOT EXISTS laravel;
GRANT ALL PRIVILEGES ON laravel.* TO 'appuser'@'%';
FLUSH PRIVILEGES;
Here is the full docker-compose.yml:
services:
mysql:
image: mysql:8.0
environment:
MYSQL_ROOT_PASSWORD: rootpassword
MYSQL_DATABASE: wordpress
MYSQL_USER: appuser
MYSQL_PASSWORD: apppassword
ports:
- "3306:3306"
volumes:
- mysql_data:/var/lib/mysql
- ./init-db.sql:/docker-entrypoint-initdb.d/init-db.sql
wordpress:
build:
context: ./wordpress
dockerfile: Dockerfile
ports:
- "8080:80"
environment:
WORDPRESS_DB_HOST: mysql
WORDPRESS_DB_USER: appuser
WORDPRESS_DB_PASSWORD: apppassword
WORDPRESS_DB_NAME: wordpress
depends_on:
- mysql
laravel:
build:
context: ./laravel
dockerfile: Dockerfile
ports:
- "8081:80"
environment:
APP_KEY: base64:generated-key-here
APP_ENV: local
APP_DEBUG: "true"
DB_CONNECTION: mysql
DB_HOST: mysql
DB_PORT: "3306"
DB_DATABASE: laravel
DB_USERNAME: appuser
DB_PASSWORD: apppassword
depends_on:
- mysql
volumes:
mysql_data:
Start the containers and complete WordPress setup
With both files in place, you can now bring up the entire stack. Docker Compose will build both app images and start all three containers on a shared network.
- Build and start all services by running:
docker compose up --build
What does this command do?
The --build flag tells Docker Compose to rebuild the WordPress and Laravel images from their Dockerfiles before starting. Without it, Compose would reuse previously cached images even if you changed a Dockerfile.
Compose runs in the foreground by default, so you will see logs streaming from all three containers. Leave this terminal running.
Wait until you see log lines indicating MySQL is ready to accept connections. This usually takes 15-30 seconds on the first run.
- Open your browser and navigate to http://localhost:8080.
You should see the WordPress installation wizard asking you to select a language.
- Select your language and click Continue.
- Enter a site title (for example, My Monorepo Site).
- Enter a username (for example, admin).
- Enter a password and confirm it.
- Enter an email address.
- Click Install WordPress.
Why didn't WordPress ask for database credentials?
The WordPress Docker image reads database connection details from the environment variables you set in docker-compose.yml (WORDPRESS_DB_HOST, WORDPRESS_DB_USER, etc.). It connects automatically, so the wizard only asks for site information.
- After seeing the success message, open a new browser tab and navigate to http://localhost:8081.
You should see the Laravel welcome page confirming the framework is running.
Seeing a connection error on either port?
- Check the terminal running docker compose up for error messages. MySQL takes a moment to initialize on first run.
- Make sure no other services are using ports 8080, 8081, or 3306 on your machine.
- If you see a Laravel error about APP_KEY, generate a real key with docker compose exec laravel php artisan key:generate --show and paste it into docker-compose.yml.
Help me troubleshoot my containers not starting.
Verify shared database connectivity
Both services should be talking to the same MySQL server. You can verify this by connecting to the MySQL container and listing its databases.
- Open a second terminal (keep the first one running docker compose up).
- List the databases by running:
docker compose exec mysql mysql -u root -prootpassword -e "SHOW DATABASES;"
What does this command do?
docker compose exec mysql runs a command inside the already-running mysql service container. The rest of the line launches the MySQL client, authenticates as root, and executes SHOW DATABASES; to print all databases.
You should see output that includes both wordpress and laravel in the list of databases.
Only see the wordpress database?
The init-db.sql script only runs during the initial MySQL setup. If you started the containers before creating the file, you need to delete the volume and restart.
- Stop the containers with Ctrl+C in the first terminal.
- Remove the volume by running docker compose down -v.
- Start again with docker compose up --build.
Help me fix my missing laravel database.
Your local development environment is running. WordPress and Laravel are both connected to a shared MySQL instance, accessible at their own ports. Next up, you will write the DigitalOcean App Platform spec to deploy this same setup to the cloud.
Write the App Platform Spec and Deploy
Your WordPress and Laravel apps are running locally with shared database access. The next challenge is getting them live on the internet behind a single domain with the same path-based routing structure.
DigitalOcean App Platform deploys containers directly from your GitHub repo using a YAML configuration file called an app spec. You will write one spec file that defines both services, their managed database connection, and the ingress rules that route traffic to the right container.
In this step, get ready to:
- Write a .do/app.yaml file defining both services, a managed MySQL database, and path-based ingress routing.
- Push the monorepo to GitHub and create an App Platform app.
- Verify both WordPress and Laravel are live on a single DigitalOcean domain.
Write the App Platform app spec
The app spec is a YAML file that tells App Platform everything it needs to deploy your app. It defines each service, where to find its Dockerfile, which database to attach, and how to route incoming requests.
- In VS Code, create a new file called app.yaml inside the .do/ directory.
- Paste the following YAML into .do/app.yaml:
name: my-monorepo-app
region: nyc
services:
# WordPress CMS served at the root path
- name: wordpress
dockerfile_path: wordpress/Dockerfile
source_dir: wordpress
github:
repo: [[GITHUB_REPO="your-username/your-repo"]]
branch: main
deploy_on_push: true
http_port: 80
instance_size_slug: apps-s-1vcpu-0.5gb
instance_count: 1
envs:
- key: WORDPRESS_DB_HOST
value: ${db.HOSTNAME}
scope: RUN_TIME
- key: WORDPRESS_DB_PORT
value: ${db.PORT}
scope: RUN_TIME
- key: WORDPRESS_DB_USER
value: ${db.USERNAME}
scope: RUN_TIME
- key: WORDPRESS_DB_PASSWORD
value: ${db.PASSWORD}
scope: RUN_TIME
type: SECRET
- key: WORDPRESS_DB_NAME
value: ${db.DATABASE}
scope: RUN_TIME
# Laravel app served at /portal
- name: laravel
dockerfile_path: laravel/Dockerfile
source_dir: laravel
github:
repo: [[GITHUB_REPO="your-username/your-repo"]]
branch: main
deploy_on_push: true
http_port: 80
instance_size_slug: apps-s-1vcpu-0.5gb
instance_count: 1
envs:
- key: APP_KEY
value: base64:your-generated-key-here
scope: RUN_TIME
type: SECRET
- key: APP_ENV
value: production
scope: RUN_TIME
- key: APP_DEBUG
value: "false"
scope: RUN_TIME
- key: DB_CONNECTION
value: mysql
scope: RUN_TIME
- key: DB_HOST
value: ${db.HOSTNAME}
scope: RUN_TIME
- key: DB_PORT
value: ${db.PORT}
scope: RUN_TIME
- key: DB_DATABASE
value: ${db.DATABASE}
scope: RUN_TIME
- key: DB_USERNAME
value: ${db.USERNAME}
scope: RUN_TIME
- key: DB_PASSWORD
value: ${db.PASSWORD}
scope: RUN_TIME
type: SECRET
# Managed MySQL database provisioned by App Platform
databases:
- name: db
engine: MYSQL
production: true
# Ingress rules control how traffic reaches each service
ingress:
rules:
- match:
path:
prefix: /portal
component:
name: laravel
preserve_path_prefix: true
- match:
path:
prefix: /
component:
name: wordpress
What does this app spec do?
The services section defines two containers. Each one points to its own source_dir and dockerfile_path so App Platform knows where to find the Dockerfile inside your monorepo.
The envs use bindable variables like ${db.HOSTNAME} and ${db.PASSWORD}. App Platform automatically injects the real database credentials at runtime. You never hardcode connection strings.
The ingress section routes traffic by URL path. The /portal rule is listed first because App Platform evaluates rules by specificity. The more specific prefix must come before the catch-all / rule. Setting preserve_path_prefix: true ensures Laravel receives the full /portal/... path instead of having it trimmed.
- Save .do/app.yaml.
YAML indentation errors?
YAML is whitespace-sensitive. Each nested level must use exactly 2 spaces (not tabs). If you see errors later during deployment, check that envs items are indented under their parent service and that the ingress section starts at the root level (zero indentation).
Help me debug a YAML indentation problem in my app spec.
✔️ Awesome, I've got everything!
Great. Make sure you have saved .do/app.yaml before moving on.
ⓧ I'd like to double check the full code
name: my-monorepo-app
region: nyc
services:
- name: wordpress
dockerfile_path: wordpress/Dockerfile
source_dir: wordpress
github:
repo: [[GITHUB_REPO="your-username/your-repo"]]
branch: main
deploy_on_push: true
http_port: 80
instance_size_slug: apps-s-1vcpu-0.5gb
instance_count: 1
envs:
- key: WORDPRESS_DB_HOST
value: ${db.HOSTNAME}
scope: RUN_TIME
- key: WORDPRESS_DB_PORT
value: ${db.PORT}
scope: RUN_TIME
- key: WORDPRESS_DB_USER
value: ${db.USERNAME}
scope: RUN_TIME
- key: WORDPRESS_DB_PASSWORD
value: ${db.PASSWORD}
scope: RUN_TIME
type: SECRET
- key: WORDPRESS_DB_NAME
value: ${db.DATABASE}
scope: RUN_TIME
- name: laravel
dockerfile_path: laravel/Dockerfile
source_dir: laravel
github:
repo: [[GITHUB_REPO="your-username/your-repo"]]
branch: main
deploy_on_push: true
http_port: 80
instance_size_slug: apps-s-1vcpu-0.5gb
instance_count: 1
envs:
- key: APP_KEY
value: base64:your-generated-key-here
scope: RUN_TIME
type: SECRET
- key: APP_ENV
value: production
scope: RUN_TIME
- key: APP_DEBUG
value: "false"
scope: RUN_TIME
- key: DB_CONNECTION
value: mysql
scope: RUN_TIME
- key: DB_HOST
value: ${db.HOSTNAME}
scope: RUN_TIME
- key: DB_PORT
value: ${db.PORT}
scope: RUN_TIME
- key: DB_DATABASE
value: ${db.DATABASE}
scope: RUN_TIME
- key: DB_USERNAME
value: ${db.USERNAME}
scope: RUN_TIME
- key: DB_PASSWORD
value: ${db.PASSWORD}
scope: RUN_TIME
type: SECRET
databases:
- name: db
engine: MYSQL
production: true
ingress:
rules:
- match:
path:
prefix: /portal
component:
name: laravel
preserve_path_prefix: true
- match:
path:
prefix: /
component:
name: wordpress
Push the monorepo to GitHub and create the app
App Platform pulls your code from GitHub, so you need to push your monorepo first. Then you will create a new app in the DigitalOcean control panel and connect it to your repository.
- Create a new repository on GitHub. Name it something recognizable (for example, wp-laravel-monorepo). Set it to Public or Private depending on your preference.
- Add the GitHub remote and push your code by running these commands:
git add .
git commit -m "Add App Platform spec"
git remote add origin https://github.com/[[GITHUB_REPO="your-username/your-repo"]].git
git branch -M main
git push -u origin main
What do these commands do?
The git add . stages all files including the new .do/app.yaml. The git remote add origin connects your local repo to GitHub so git push knows where to send your code.
Push rejected or authentication error?
Make sure you created the GitHub repository first. If you see an authentication error, you may need to set up a Personal Access Token or SSH key for GitHub access.
Help me fix a git push authentication error to GitHub.
Now create the app on DigitalOcean App Platform.
- Open the DigitalOcean Control Panel at cloud.digitalocean.com.
- Click Create, then select App Platform from the dropdown menu.
- Under Service Provider, select GitHub. If prompted, grant DigitalOcean read access to your GitHub account.
- Under Repository, select the monorepo you just pushed. Ensure the branch is set to main.
- App Platform will detect your Dockerfiles. On the Resources page, verify it found both the WordPress and Laravel components.
- Click Edit beside each component to confirm the Source Directory matches (wordpress and laravel respectively).
- Expand Add Resource (Optional) and select Database. Choose an existing managed MySQL database from your account, or create one. App Platform will make the connection details available via environment variables.
- On the Environment page, add your environment variables using the bindable format (${db.HOSTNAME}, ${db.PORT}, etc.) as defined in your app spec.
- On the Info page, set your app name and choose the New York region to match the nyc value in your app spec.
- Review the summary and pricing, then click Create Resources.
Why bindable variables instead of hardcoded strings?
Bindable variables like ${db.HOSTNAME} are resolved dynamically by App Platform at deploy time. If your database is ever replaced, rotated, or migrated, App Platform injects the new credentials automatically. You never need to update environment variables manually.
Verify the deployment
App Platform builds both containers from your Dockerfiles and deploys them. This takes a few minutes per component.
- In the DigitalOcean dashboard, navigate to your app's Overview page.
- Watch the Deployments tab. Click View Logs to monitor the build progress for each component.
- Wait until both the WordPress and Laravel components show Active status.
How long does this take?
The first build typically takes 3 to 8 minutes per component because App Platform pulls base images and installs dependencies from scratch. Subsequent deployments are faster due to layer caching.
- Copy the app URL shown at the top of the Overview page. It looks like your-app.ondigitalocean.app.
- Open your-app.ondigitalocean.app/ in your browser. You should see the WordPress setup screen or homepage.
- Open your-app.ondigitalocean.app/portal in your browser. You should see the Laravel welcome page.
Seeing a 502 or 503 error?
A 502 usually means the container is still starting up. Wait 30 seconds and refresh. If the error persists, check the Runtime Logs in the DigitalOcean dashboard for connection errors or missing environment variables.
A 404 on the /portal path means the ingress rule may not be applied yet. Confirm that /portal appears before / in your .do/app.yaml ingress rules.
Help me debug my App Platform deployment that shows 502 or 503 errors.
Secret mission
Add Health Checks and Autoscaling
Your app is live, but production needs resilience. Can you add a health check endpoint so App Platform knows when your Laravel container is healthy, then configure autoscaling so it handles traffic spikes automatically?
Clean Up Your Resources
Clean Up Your Resources
Decide whether to keep your resources running, pause them to come back later, or delete them entirely. This project uses paid DigitalOcean resources that cost approximately $25/month for the App Platform containers and managed MySQL database.
Cost warning
Your DigitalOcean App Platform app ($10/month for 2 containers) and Managed MySQL database ($15/month) will continue billing as long as they exist. If you are not actively using these resources, archive or destroy them to avoid ongoing charges.
Resources you used:
- DigitalOcean App Platform app (2 containers: WordPress and Laravel).
- DigitalOcean Managed MySQL database cluster.
- Local monorepo project files on your Desktop.
- Local Docker images and volumes.
- GitHub repository.
Keep everything running
No action needed. Choose this if you are still actively building on this project or plan to use WordPress and Laravel in production.
- Your App Platform app will continue running and billing at approximately $10/month for the two containers.
- Your Managed MySQL database will continue billing at $15/month.
- Local Docker images and volumes remain on your machine but consume disk space.
- Your GitHub repository remains available for future pushes and deployments.
Pause - I'll come back to this later
Archive your app to stop container billing while preserving your configuration. You can restore it later with one click.
- In the DigitalOcean Control Panel, click Apps from the left menu, then click your app.
- Click the Settings tab.
- Scroll down to the Archive mode section and click Archive.
- Enter the app's name to confirm, then click Archive.
- To restore later, return to the same page and click Restore.
Archiving does not stop database billing
Archiving your app stops billing for the App Platform containers, but your Managed MySQL database continues to bill at $15/month. If you want to pause database costs too, you will need to destroy the database cluster separately (see the Delete tab for those steps).
- Stop local Docker containers by running:
- Your local files and GitHub repository remain intact for when you return.
docker compose down
This stops and removes the running containers but preserves your images and volume data.
Delete - I don't want to use this again
Remove all project resources and start fresh. This permanently deletes your deployed app, database, local files, and Docker resources.
Destroy your App Platform app:
- In the DigitalOcean Control Panel, go to the Apps page and select your app.
- Click the Settings tab.
- Scroll to the bottom of the page and click Destroy.
- Enter the app's name to confirm, then click Destroy.
Destroy your Managed MySQL database:
- In the DigitalOcean Control Panel, go to the Apps page and select your app.
- Click the Settings tab and select the database component.
- At the bottom of the page, click Destroy Database.
- Enter the database's name to confirm, then click Destroy.
Remove local Docker resources:
- Stop containers and remove volumes by running:
docker compose down -v
- Remove the Docker images built for this project by running:
docker image rm $(docker image ls --filter reference='*wordpress*' --filter reference='*laravel*' -q) 2>/dev/null
Delete local project files:
- Remove the monorepo folder from your Desktop by running:
cd ~/Desktop
rm -rf my-monorepo-app
On Windows, use:
cd ~\Desktop
rmdir /s /q my-monorepo-app
Delete the GitHub repository:
- Go to your repository on GitHub.
- Click Settings at the top of the repository page.
- Scroll to the bottom and click Delete this repository.
- Follow the confirmation prompts to permanently delete the repository.
That's a Wrap!
That's a Wrap!
Nice work! You just built and deployed a production-ready monorepo with two containerized applications running behind a single domain on DigitalOcean App Platform.
You've learned how to:
- Built a monorepo with WordPress and Laravel in separate subdirectories, each with its own Dockerfile and independent container configuration.
- Created a Docker Compose setup for local development with a shared MySQL database, giving both apps a consistent data layer without managing separate instances.
- Wrote a DigitalOcean App Platform app spec with path-based ingress routing and deployed both applications to production on a single domain with managed MySQL.
- Secret Mission: Added a health check endpoint and configured autoscaling rules for production resilience, so your Laravel service scales automatically under load.
Ready to quiz yourself?