Running a Laravel App into Apache server on Docker container, based on Alpine Linux

Hi there!

I’ve been continuing the development of my Laravel app. Nowadays, most web applications are meant to be Dockerized, instead of running directly on the native OS installation of a server. So, at some point during this development process, I decided to pack my Laravel app into a multi-container Docker app.

Initially, I installed Laravel Sail on my project using Composer. This provided a starting point, as the package automatically generates the necessary files to build the Docker images required to run the app from the generated Docker containers, rather than from the native OS installation. Additionally, this installation generates the necessary folders in the project’s base directory, containing not only the Dockerfiles but also other referenced files required to properly build the Docker images. It also generates a ‘docker-compose.yml’ file, used by Docker Compose to set up all these containers to work together, ensuring proper integration, setting up networks and volumes, and mapping network ports as needed, allowing us to access all the app’s features from the host.

Generally, the files generated by default when installing Laravel Sail work fine for running the app using the ‘sail up -d’ command from the project’s root directory. However, these auto-generated files don’t always match the developer’s desired outcome. For example, if we want to use external tools like phpMyAdmin to manage the app’s database, we need to manually edit the ‘docker-compose.yml’ file, as these tools aren’t included by default in the Laravel Sail installation.

In my case, I put in considerable effort to achieve the desired result. The generated images and their corresponding containers in my app, which included the main Laravel app, the MariaDB image, and the phpMyAdmin image, were initially built based on an Ubuntu image. I worked extensively to code the Dockerfiles and other necessary files to build these images to function as before but based on an Alpine Linux image instead of Ubuntu or Debian. As a result, the images built from my custom Dockerfiles work just the same as the previous ones, but their size is three times smaller, significantly optimizing disk usage with identical functionality.

I had previously made a post asking for the best way to achieve this result and finally posted my success when I achieved my goal:

Making lighter Docker images on Laravel Sail project

On there, you can see the code I had at that point.

However, another important issue has to do with the way in which the main Laravel app image is built and then runs within a container. By default, the main Dockerfile used to build the main Laravel app image uses the ‘php artisan serve’ command to run the entire Laravel app. This means it uses a PHP server instead of a full HTTP server, such as Apache or Nginx. Despite the PHP server generally working fine, it is meant to be used during the development stage and not in production.
In addition to that, there are some Apache images on DockerHub ready to work with a Laravel app, but most of them are based on Ubuntu instead of Alpine Linux.

On my computer, I have Linux Mint 21.3 “Victoria,” based on Ubuntu 22.04 “Jammy Jellyfish” LTS, making it almost fully compatible with Ubuntu. In my native OS installation, I have installed the Apache HTTP Server v2.4.52. In my Apache installation, I have an SSL module installed and enabled, with the file:

‘/etc/apache2/mods-available/ssl.conf’

So, I’ve configured a VirtualHost that defines a URL for my application using the HTTPS protocol (dependent on SSL) instead of the standard HTTP protocol.

Here’s my VirtualHost config file for my website (currently for local deployment only):

‘/etc/apache2/sites-enabled/infoalquiler.conf’:

<VirtualHost *:443>

    SSLEngine on
    SSLCertificateFile /etc/apache2/ssl/apache.crt
    SSLCertificateKeyFile /etc/apache2/ssl/apache.key
    ServerAdmin leandrocaplan@gmail.com
    ServerName infoalquiler.com.ar
    ServerAlias www.infoalquiler.com.ar

    DocumentRoot /var/www/html/infoalquiler/public

    <Directory />
            Options FollowSymLinks
            AllowOverride None
    </Directory>
    <Directory /var/www/html/infoalquiler>
        Options -Indexes +FollowSymLinks +MultiViews
        AllowOverride All
        Require all granted
    </Directory>

    ErrorLog ${APACHE_LOG_DIR}/error.log
    CustomLog ${APACHE_LOG_DIR}/access.log combined

</VirtualHost>

So, I can access all the features of my app by typing this URL into my browser, along with any necessary sub-paths (which only work on my local computer):

https://www.infoalquiler.com.ar/

What I’m trying to achieve is to be able, simply by typing that same URL into my browser, to access my Dockerized app. To accomplish this, I need to properly build and configure an Alpine-based Docker image with Apache installed, and run my Laravel app through Apache instead of the PHP server set by default in Laravel Sail. I’m working towards this goal by carefully coding the corresponding Dockerfile and including the necessary configuration files in that containerized setup. In my attempts to achieve this, I’ve discovered that the configuration files for Apache installations on Alpine Linux are quite different from those for Apache installations on Ubuntu or Debian-based distributions.

At the moment, I have a ‘docker-compose.yml’ file with two lines commented out. When attempting to build the Apache-based Docker image for my Laravel app, I uncomment those lines and comment out the other two. I follow the reverse procedure when building the default image that uses the PHP server, as shown here:

‘docker-compose.yml’:

version: "1.0"
services:
    laravel.test:
        build:
            context: ./
            dockerfile: ./docker/8.3/Dockerfile           #Uncommented when using current Dockerfile, with PHP server
            #dockerfile: ./docker/8.3-apache/Dockerfile   #Uncommented when using Dockerfile attempting to run the app on Apache server
            args:
                WWWGROUP: '${WWWGROUP}'
        image: sail-8.3/app
        
        extra_hosts:
            - 'host.docker.internal:host-gateway'
        ports:
            - '${APP_PORT:-80}:80'
            - '${VITE_PORT:-5173}:${VITE_PORT:-5173}'
        environment:
            WWWUSER: '${WWWUSER}'
            LARAVEL_SAIL: 1
            XDEBUG_MODE: '${SAIL_XDEBUG_MODE:-off}'
            XDEBUG_CONFIG: '${SAIL_XDEBUG_CONFIG:-client_host=host.docker.internal}'
            IGNITION_LOCAL_SITES_PATH: '${PWD}'
            TZ: 'America/Argentina/Buenos_Aires'
            SUPERVISOR_PHP_COMMAND: '/usr/bin/php -d variables_order=EGPCS /var/www/html/artisan serve --host=0.0.0.0 --port=80'
            SUPERVISOR_PHP_USER: 'sail'
        volumes:
            - '.:/var/www/html'                         #Uncommented when using current Dockerfile, with PHP server
            #- '.:/var/www/localhost/htdocs'            #Uncommented when using Dockerfile attempting to run the app on Apache server
        networks:
            - sail
        depends_on:
            - mariadb
            - phpmyadmin
    mariadb:
        image: yobasystems/alpine-mariadb:10
        ports:
            - '${FORWARD_DB_PORT:-3306}:3306'
        environment:
            MYSQL_ROOT_PASSWORD: '${DB_PASSWORD}'
            MYSQL_ROOT_HOST: '%'
            MYSQL_DATABASE: '${DB_DATABASE}'
            MYSQL_USER: '${DB_USERNAME}'
            MYSQL_PASSWORD: '${DB_PASSWORD}'
            MYSQL_ALLOW_EMPTY_PASSWORD: 'yes'
        volumes:
            - 'sail-mariadb:/var/lib/mysql'
            - './docker/mysql/create-testing-database.sh:/docker-entrypoint-initdb.d/10-create-testing-database.sh'
        networks:
            - sail
        healthcheck:
            test:
                - CMD
                - mysqladmin
                - ping
                - '-p${DB_PASSWORD}'
            retries: 3
            timeout: 5s

    phpmyadmin:
      build:
        context: ./
        dockerfile: ./docker/phpmyadmin/Dockerfile
        args:
            WWWGROUP: '${WWWGROUP}'
      image: sail-phpmyadmin/app

      ports:
          - 8081:80
      networks:
          - sail
      environment:
          - PMA_ARBITRARY=1
          - SUPERVISOR_PHP_USER=sail
networks:
    sail:
        driver: bridge
volumes:
    sail-mariadb:
        driver: local

I can perfectly run my app when built with the corresponding lines for the PHP Server in the ‘.yml’ file, by the URL:

localhost:8001/(sub-path)…

I can also access phpMyAdmin by the URL:

‘localhost:8081’

However, when I uncomment the lines corresponding to the attempt to build the Apache image, I encounter unexpected results. Initially, I could access the root URL fine by typing ‘localhost:8001’ in the browser, but then encountered errors when trying to access a sub-path.

Subsequently, on further attempts, I encountered this permission error:

ErrorException
file_put_contents(/var/www/localhost/htdocs/storage/framework/views/3e0[…]dd.php): Failed to open stream: Permission denied

Here’s the correspondent Dockerfile:

‘docker/8.3-apache/Dockerfile’:

FROM alpine

# Instalar dependencias
RUN apk update && apk add --no-cache \
apache2 curl gnupg ca-certificates zip unzip git supervisor libpng-dev libjpeg librsvg \
php83 php83-xmlreader php83-mysqli php83-zip php83-apache2 php83-cli php83-dev php83-pdo_pgsql php83-gd php83-curl php83-xml php83-mbstring \
php83-openssl php83-json php83-dom php83-ctype php83-session php83-fileinfo php83-xmlwriter php83-simplexml php83-tokenizer php83-pdo_mysql php83-phar \
nodejs npm mysql-client dpkg openssl shadow libcap && \
echo "ServerName localhost" >> /etc/apache2/httpd.conf && \
cp /usr/bin/php83 /usr/bin/php && \
setcap "cap_net_bind_service=+ep" /usr/bin/php && \
rm -rf /var/cache/apk/*

RUN curl -sLS https://getcomposer.org/installer | php -- --install-dir=/usr/bin/ --filename=composer    

WORKDIR /var/www/localhost/htdocs

RUN mkdir /var/www/localhost/htdocs/tmp && \
chmod -R 777 /var/www/localhost/htdocs/tmp && \
rm -rf /var/cache/apk/*

ARG WWWGROUP=1000
ARG USER_ID=1337

RUN addgroup -g $WWWGROUP sail && adduser -D -s /bin/sh -G sail -u $USER_ID sail

RUN rm -f index.html && rm -f /etc/apache2/httpd.conf
COPY ./docker/8.3-apache/httpd.conf /etc/apache2/httpd.conf

RUN echo -e "<?php\nphpinfo();\n?>" >> ./info.php
RUN chown -R apache:apache .
RUN chmod -R 777 .

CMD ["httpd", "-D", "FOREGROUND"]

And then, this config file in the same directory (I’ve deleted the commented lines to avoid the posted code becoming too lengthy).

‘docker/8.3-apache/httpd.conf’ (code line 118 it’s the most relevant, setting up a VirtualHost):

ServerTokens OS
ServerRoot /var/www
Listen 80


#LoadModule mpm_event_module modules/mod_mpm_event.so
LoadModule mpm_prefork_module modules/mod_mpm_prefork.so
#LoadModule mpm_worker_module modules/mod_mpm_worker.so
LoadModule authn_file_module modules/mod_authn_file.so
#LoadModule authn_dbm_module modules/mod_authn_dbm.so
#LoadModule authn_anon_module modules/mod_authn_anon.so
#LoadModule authn_dbd_module modules/mod_authn_dbd.so
#LoadModule authn_socache_module modules/mod_authn_socache.so
LoadModule authn_core_module modules/mod_authn_core.so
LoadModule authz_host_module modules/mod_authz_host.so
LoadModule authz_groupfile_module modules/mod_authz_groupfile.so
LoadModule authz_user_module modules/mod_authz_user.so
#LoadModule authz_dbm_module modules/mod_authz_dbm.so
#LoadModule authz_owner_module modules/mod_authz_owner.so
#LoadModule authz_dbd_module modules/mod_authz_dbd.so
LoadModule authz_core_module modules/mod_authz_core.so
LoadModule access_compat_module modules/mod_access_compat.so
LoadModule auth_basic_module modules/mod_auth_basic.so
#LoadModule auth_form_module modules/mod_auth_form.so
#LoadModule auth_digest_module modules/mod_auth_digest.so
#LoadModule allowmethods_module modules/mod_allowmethods.so
#LoadModule file_cache_module modules/mod_file_cache.so
#LoadModule cache_module modules/mod_cache.so
#LoadModule cache_disk_module modules/mod_cache_disk.so
#LoadModule cache_socache_module modules/mod_cache_socache.so
#LoadModule socache_shmcb_module modules/mod_socache_shmcb.so
#LoadModule socache_dbm_module modules/mod_socache_dbm.so
#LoadModule socache_memcache_module modules/mod_socache_memcache.so
#LoadModule socache_redis_module modules/mod_socache_redis.so
#LoadModule watchdog_module modules/mod_watchdog.so
#LoadModule macro_module modules/mod_macro.so
#LoadModule dbd_module modules/mod_dbd.so
#LoadModule dumpio_module modules/mod_dumpio.so
#LoadModule echo_module modules/mod_echo.so
#LoadModule buffer_module modules/mod_buffer.so
#LoadModule data_module modules/mod_data.so
#LoadModule ratelimit_module modules/mod_ratelimit.so
LoadModule reqtimeout_module modules/mod_reqtimeout.so
#LoadModule ext_filter_module modules/mod_ext_filter.so
#LoadModule request_module modules/mod_request.so
#LoadModule include_module modules/mod_include.so
LoadModule filter_module modules/mod_filter.so
#LoadModule reflector_module modules/mod_reflector.so
#LoadModule substitute_module modules/mod_substitute.so
#LoadModule sed_module modules/mod_sed.so
#LoadModule charset_lite_module modules/mod_charset_lite.so
#LoadModule deflate_module modules/mod_deflate.so
#LoadModule brotli_module modules/mod_brotli.so
LoadModule mime_module modules/mod_mime.so
LoadModule log_config_module modules/mod_log_config.so
#LoadModule log_debug_module modules/mod_log_debug.so
#LoadModule log_forensic_module modules/mod_log_forensic.so
#LoadModule logio_module modules/mod_logio.so
LoadModule env_module modules/mod_env.so
#LoadModule mime_magic_module modules/mod_mime_magic.so
#LoadModule expires_module modules/mod_expires.so
LoadModule headers_module modules/mod_headers.so
#LoadModule usertrack_module modules/mod_usertrack.so
#LoadModule unique_id_module modules/mod_unique_id.so
LoadModule setenvif_module modules/mod_setenvif.so
LoadModule version_module modules/mod_version.so
#LoadModule remoteip_module modules/mod_remoteip.so
#LoadModule session_module modules/mod_session.so
#LoadModule session_cookie_module modules/mod_session_cookie.so
#LoadModule session_crypto_module modules/mod_session_crypto.so
#LoadModule session_dbd_module modules/mod_session_dbd.so
#LoadModule slotmem_shm_module modules/mod_slotmem_shm.so
#LoadModule slotmem_plain_module modules/mod_slotmem_plain.so
#LoadModule dialup_module modules/mod_dialup.so
#LoadModule http2_module modules/mod_http2.so
LoadModule unixd_module modules/mod_unixd.so
#LoadModule heartbeat_module modules/mod_heartbeat.so
#LoadModule heartmonitor_module modules/mod_heartmonitor.so
LoadModule status_module modules/mod_status.so
LoadModule autoindex_module modules/mod_autoindex.so
#LoadModule asis_module modules/mod_asis.so
#LoadModule info_module modules/mod_info.so
#LoadModule suexec_module modules/mod_suexec.so

<IfModule !mpm_prefork_module>
	#LoadModule cgid_module modules/mod_cgid.so
</IfModule>

<IfModule mpm_prefork_module>
	#LoadModule cgi_module modules/mod_cgi.so
</IfModule>

#LoadModule vhost_alias_module modules/mod_vhost_alias.so
#LoadModule negotiation_module modules/mod_negotiation.so
LoadModule dir_module modules/mod_dir.so
#LoadModule actions_module modules/mod_actions.so
#LoadModule speling_module modules/mod_speling.so
#LoadModule userdir_module modules/mod_userdir.so
LoadModule alias_module modules/mod_alias.so
#LoadModule rewrite_module modules/mod_rewrite.so

LoadModule negotiation_module modules/mod_negotiation.so

<IfModule unixd_module>
  User apache
  Group apache
</IfModule>

ServerAdmin you@example.com
ServerSignature On

<Directory />
  AllowOverride none
  Require all denied
</Directory>


<VirtualHost *:80>

  DocumentRoot "/var/www/localhost/htdocs/public"
  #ServerAdmin leandrocaplan@gmail.com
  #ServerName infoalquiler.com.ar
  #ServerAlias www.infoalquiler.com.ar

  <Directory />
    Options FollowSymLinks
    AllowOverride None
  </Directory>

  <Directory "/var/www/localhost/htdocs">
    Options -Indexes +FollowSymLinks +MultiViews
    AllowOverride All
    Require all granted
  </Directory>

</VirtualHost>

<IfModule dir_module>
    DirectoryIndex index.html
</IfModule>

<Files ".ht*">
    Require all denied
</Files>

ErrorLog logs/error.log

LogLevel warn

<IfModule log_config_module>

  LogFormat "%h %l %u %t \"%r\" %>s %b \"%{Referer}i\" \"%{User-Agent}i\"" combined
  LogFormat "%h %l %u %t \"%r\" %>s %b" common

  <IfModule logio_module>
    # You need to enable mod_logio.c to use %I and %O
    LogFormat "%h %l %u %t \"%r\" %>s %b \"%{Referer}i\" \"%{User-Agent}i\" %I %O" combinedio
  </IfModule>

  CustomLog logs/access.log combined
</IfModule>

<IfModule alias_module>
  ScriptAlias /cgi-bin/ "/var/www/localhost/cgi-bin/"
</IfModule>

<IfModule cgid_module>
    
</IfModule>

<Directory "/var/www/localhost/cgi-bin">
    AllowOverride None
    Options None
    Require all granted
</Directory>

<IfModule headers_module>
  RequestHeader unset Proxy early
</IfModule>

<IfModule mime_module>
    TypesConfig /etc/apache2/mime.types

    AddType application/x-compress .Z
    AddType application/x-gzip .gz .tgz

</IfModule>

<IfModule mime_magic_module>
    MIMEMagicFile /etc/apache2/magic
</IfModule>

IncludeOptional /etc/apache2/conf.d/*.conf
ServerName localhost

Initially, when I attempted this, these lines in the Dockerfile:

RUN chown -R apache:apache .
RUN chmod -R 777 .

Changed the owner and permissions in my host folder, mounted as a volume in my ‘docker-compose.yml’. However, in subsequent attempts, it didn’t even do that, resulting in this issue in my host filesystem: My own user and my own group (leandro:leandro) owns the host root project folder, and gets full permissions of read and write, while others has only read permissions.

Despite reading numerous documentation resources, I’m still feeling quite stuck. I’m struggling to find the proper way to achieve the result I’m aiming for. Can anyone provide any insight into what I might be overlooking or suggest the steps to get back on track towards my goal?

I tried to add some other images and hyperlinks to this post, to show better the results I’m getting and the documentation I’ve read, but I couldn’t since I’m a pretty new user, and I don’t have such privileges.

I believe someone might be able to offer assistance. If more information is needed or if there’s anything that isn’t clear, please let me know.

Thanks a lot!

Leandro

Can you give a summary of your question?

Yes. I’m trying to Dockerize my Laravel app, instead of running it from my native installation of Apache over Linux Mint (Ubuntu based). There I’ve posted the steps I’ve followed while attempting that.
I’d like to know what would be the better approach to achieve that desired result.

My personal recommendations:

I would not start with a blank alpine image. If you are using PHP, there is a php:apache image which includes the necessary web server. That image I would run without TLS/SSL.

Then I would run a proxy server to enable and terminate TLS/SSL, and route requests by sub-domain to potential target services. I like to use nginx-proxy with companion on single host and Traefik for Swarm, both enable configuration discovery with env/labels respectively.