Status: available for projects [email protected]
← Back to articles

How I deploy Laravel apps using Coolify and Docker

Laravel ecosystem is rich especially when it comes to deploying the application to production. You can choose using first-party solutions such as Forge or Cloud. Unfortunately those can get very expensive or inflexible when it comes to deploying and running multiple projects and databases.

Why I Chose Coolify

Coolify caught my attention due to its simplicity and powerful capabilities. As a self-hostable platform-as-a-service (PaaS), it abstracts many complexities involved in deploying applications while allowing full control over the server environment. Its intuitive UI, comprehensive documentation, and community support made the onboarding process smooth. Additionally, Coolify integrates seamlessly with version control systems, making deployments automated and efficient.

Why I Use Docker

Docker plays a crucial role in my deployment strategy. It allows me to containerize my Laravel applications, ensuring consistency across different environments and across multiple services that I need to run a production Laravel app - Queue workers, Schedulers, Nightwatch agent, etc..

What This Setup Gives Me

Combining Coolify with Docker offers several advantages:

  1. Consistent Environments: Every deployment maintains the same configurations, reducing bugs and deployment issues.

  2. Scalability: Docker’s lightweight containers paired with Coolify’s orchestration capabilities make scaling straightforward.

  3. Automated Deployments: Integration with Git allows for continuous deployment workflows.

Sample Dockerfile

FROM oven/bun:1 AS bun-source

FROM serversideup/php:8.5-frankenphp AS web
ENV PHP_OPCACHE_ENABLE=1
USER root
RUN install-php-extensions intl bcmath
COPY --from=bun-source /usr/local/bin/bun /usr/local/bin/bun
COPY --chown=www-data:www-data . /var/www/html
USER www-data
RUN composer install --no-interaction --optimize-autoloader --no-dev
RUN bun install && bun run build && rm -rf /var/www/html/.bun
RUN rm -rf /var/www/html/.composer/cache

Sample docker-compose.yml

x-web-image: &web-image
  build:
    context: .
    dockerfile: Dockerfile
    target: web

x-app-volumes: &app-volumes
  volumes:
    - 'app-storage:/var/www/html/storage:noexec,nosuid,nodev'
  tmpfs:
    - /tmp:noexec,nosuid,nodev,mode=1777

x-security-opts: &security-opts
  security_opt:
    - no-new-privileges:true
  cap_drop:
    - ALL
  cap_add:
    - NET_BIND_SERVICE
    - CHOWN
    - SETGID
    - SETUID
    - DAC_OVERRIDE

x-resource-limits: &resource-limits
  deploy:
    resources:
      limits:
        memory: 512M
      reservations:
        memory: 128M

x-logging: &logging
  logging:
    driver: "json-file"
    options:
      max-size: "10m"
      max-file: "3"

x-app-env-file: &app-env-file
  env_file:
    - .env

x-docker-environment: &docker-environment
  AUTORUN_ENABLED: true

networks:
  app-network:
    driver: bridge
    internal: false


services:
  app:
    <<: [*web-image, *app-volumes, *security-opts, *resource-limits, *logging, *app-env-file]
    environment:
      <<: *docker-environment
      PHP_FPM_POOL_NAME: "app"
    command: ["php", "artisan", "octane:start", "--server=frankenphp", "--port=8080"]
    labels:
      - "traefik.enable=true"
      - "traefik.http.routers.${TRAEFIK_SERVICE_NAME}.rule=Host(`${APP_DOMAIN}`)"
      - "traefik.http.routers.${TRAEFIK_SERVICE_NAME}.entrypoints=https"
      - "traefik.http.routers.${TRAEFIK_SERVICE_NAME}.tls=true"
      - "traefik.http.routers.${TRAEFIK_SERVICE_NAME}.tls.certresolver=letsencrypt"
      - "traefik.http.services.${TRAEFIK_SERVICE_NAME}.loadbalancer.server.port=8080"
    healthcheck:
      test: ["CMD", "healthcheck-octane"]
      interval: 30s
      timeout: 10s
      retries: 3
      start_period: 10s
    restart: unless-stopped
    networks:
      - app-network
    stop_signal: SIGTERM
    stop_grace_period: 10s

Some Drawbacks and Potential Pitfalls

While this setup offers many benefits, here are the real challenges I've encountered:

  1. Build vs Runtime Environment Variables: Decoupling build-time variables from runtime variables is not straightforward.

  2. Storage Permissions: Every new project requires manual setup of storage directory permissions. The mounted volume needs correct ownership for the www-data user, which isn't automatically handled.

  3. Traefik Debugging: When deployments fail due to routing issues, Traefik's configuration and logs aren't easily accessible through Coolify's UI. Debugging why your app isn't reachable requires SSH-ing into the server and inspecting Traefik directly.

  4. Database Management: Updating databases that were set up using Coolify's preconfigured applications (like PostgreSQL or MySQL) isn't well documented. Version upgrades or configuration changes require understanding Coolify's internal structure.

Conclusion

Deploying Laravel applications using Coolify and Docker has transformed my development workflow. This combination offers a robust, scalable, and efficient deployment strategy. While there are pitfalls to watch out for, the benefits far outweigh the drawbacks, making it an excellent choice for modern Laravel deployments.

Email me CV ↓