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:
Consistent Environments: Every deployment maintains the same configurations, reducing bugs and deployment issues.
Scalability: Docker’s lightweight containers paired with Coolify’s orchestration capabilities make scaling straightforward.
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:
Build vs Runtime Environment Variables: Decoupling build-time variables from runtime variables is not straightforward.
Storage Permissions: Every new project requires manual setup of storage directory permissions. The mounted volume needs correct ownership for the
www-datauser, which isn't automatically handled.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.
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.