Skip to content

Deploying a Symfony App

What is Symfony?

Symfony is a mature, enterprise-grade PHP framework for building web applications and APIs. Known for its flexibility, reusability, and comprehensive ecosystem, Symfony is used by thousands of companies worldwide to build robust, scalable applications.

Key features include:

  • Flexible MVC architecture with decoupled components
  • Powerful routing system with advanced parameter conversion
  • Doctrine ORM for database abstraction and object mapping
  • Built-in security framework (authentication and authorization)
  • Comprehensive form handling and validation
  • Event dispatcher for loose coupling
  • Service container and dependency injection
  • Console component for CLI commands
  • HTTP caching with reverse proxy support
  • Cache abstraction (Redis, Memcached, File)
  • Logging with Monolog integration
  • API Platform for rapid API development
  • Webpack Encore for frontend asset management
  • Database migrations with Doctrine
  • Testing utilities (PHPUnit, WebTestCase)
  • Messenger for async processing
  • Mailer component for email handling
  • Code generation tools
  • Comprehensive documentation
  • Active community and ecosystem

Symfony is ideal for building enterprise web applications, RESTful APIs, single-page applications, microservices, and large-scale business systems.

Prerequisites

Before deploying a Symfony application to Klutch.sh, ensure you have:

  • PHP 8.2 or later installed on your local machine
  • Composer for dependency management
  • Git and a GitHub account
  • A Klutch.sh account with dashboard access
  • PostgreSQL or MySQL for data persistence
  • Node.js and npm for frontend assets (optional)
  • Basic understanding of PHP and web architecture

Getting Started with Symfony

Step 1: Create a Symfony Project

Using Composer, create a new Symfony project:

Terminal window
composer create-project symfony/skeleton symfony-app
cd symfony-app

For a full-stack application with forms, security, and other utilities:

Terminal window
composer create-project symfony/website-skeleton symfony-app
cd symfony-app

This creates a project structure:

symfony-app/
├── bin/
│ └── console
├── config/
│ ├── packages/
│ ├── routes.yaml
│ └── services.yaml
├── public/
│ └── index.php
├── src/
│ ├── Controller/
│ ├── Entity/
│ ├── Repository/
│ ├── Form/
│ └── Kernel.php
├── templates/
├── tests/
├── var/
│ ├── cache/
│ └── log/
├── .env
├── .env.local
├── composer.json
└── symfony.lock

Step 2: Configure Environment

Create a .env file:

APP_ENV=dev
APP_SECRET=your_secret_key_here
APP_DEBUG=1
# Database
DATABASE_URL="postgresql://postgres:password@127.0.0.1:5432/symfony_db?serverVersion=15&charset=utf8"
# Or for MySQL:
# DATABASE_URL="mysql://root:password@127.0.0.1:3306/symfony_db?serverVersion=8.0&charset=utf8mb4"
# Redis
REDIS_URL=redis://127.0.0.1:6379
# Messenger
MESSENGER_TRANSPORT_DSN=redis://127.0.0.1:6379/messages

Step 3: Create Database Entity

Create an Item entity:

Terminal window
php bin/console make:entity Item

This generates src/Entity/Item.php:

<?php
namespace App\Entity;
use App\Repository\ItemRepository;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Validator\Constraints as Assert;
#[ORM\Entity(repositoryClass: ItemRepository::class)]
#[ORM\Table(name: 'items')]
class Item
{
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column]
private ?int $id = null;
#[ORM\Column(length: 100, unique: true)]
#[Assert\NotBlank]
#[Assert\Length(min: 1, max: 100)]
private ?string $name = null;
#[ORM\Column(type: 'text', nullable: true)]
#[Assert\Length(max: 500)]
private ?string $description = null;
#[ORM\Column]
#[Assert\NotBlank]
#[Assert\Positive]
private ?int $price = null;
#[ORM\Column]
private ?\DateTimeImmutable $createdAt = null;
#[ORM\Column]
private ?\DateTimeImmutable $updatedAt = null;
public function __construct()
{
$this->createdAt = new \DateTimeImmutable();
$this->updatedAt = new \DateTimeImmutable();
}
public function getId(): ?int
{
return $this->id;
}
public function getName(): ?string
{
return $this->name;
}
public function setName(string $name): self
{
$this->name = $name;
return $this;
}
public function getDescription(): ?string
{
return $this->description;
}
public function setDescription(?string $description): self
{
$this->description = $description;
return $this;
}
public function getPrice(): ?int
{
return $this->price;
}
public function setPrice(int $price): self
{
$this->price = $price;
return $this;
}
public function getCreatedAt(): ?\DateTimeImmutable
{
return $this->createdAt;
}
public function getUpdatedAt(): ?\DateTimeImmutable
{
return $this->updatedAt;
}
public function setUpdatedAt(\DateTimeImmutable $updatedAt): self
{
$this->updatedAt = $updatedAt;
return $this;
}
}

Step 4: Create Repository

Generate the repository:

Terminal window
php bin/console make:repository ItemRepository

Edit src/Repository/ItemRepository.php:

<?php
namespace App\Repository;
use App\Entity\Item;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Doctrine\Persistence\ManagerRegistry;
/**
* @extends ServiceEntityRepository<Item>
*/
class ItemRepository extends ServiceEntityRepository
{
public function __construct(ManagerRegistry $registry)
{
parent::__construct($registry, Item::class);
}
public function findBySearch(string $query): array
{
return $this->createQueryBuilder('i')
->where('i.name LIKE :query')
->orWhere('i.description LIKE :query')
->setParameter('query', '%' . $query . '%')
->getQuery()
->getResult();
}
public function findAllPaginated(int $page = 1, int $limit = 10): array
{
return $this->createQueryBuilder('i')
->setFirstResult(($page - 1) * $limit)
->setMaxResults($limit)
->orderBy('i.createdAt', 'DESC')
->getQuery()
->getResult();
}
}

Step 5: Create API Controller

Generate a controller:

Terminal window
php bin/console make:controller Api/ItemController

Edit src/Controller/Api/ItemController.php:

<?php
namespace App\Controller\Api;
use App\Entity\Item;
use App\Repository\ItemRepository;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\Validator\Validator\ValidatorInterface;
#[Route('/api', name: 'api_')]
class ItemController extends AbstractController
{
public function __construct(
private ItemRepository $itemRepository,
private EntityManagerInterface $entityManager,
private ValidatorInterface $validator,
) {
}
#[Route('/health', name: 'health', methods: ['GET'])]
public function health(): JsonResponse
{
return $this->json([
'status' => 'healthy',
'service' => 'symfony-app'
]);
}
#[Route('/items', name: 'items_list', methods: ['GET'])]
public function list(Request $request): JsonResponse
{
$page = $request->query->getInt('page', 1);
$limit = $request->query->getInt('limit', 10);
$search = $request->query->getString('search', '');
$items = $search
? $this->itemRepository->findBySearch($search)
: $this->itemRepository->findAllPaginated($page, $limit);
return $this->json([
'data' => $items,
'page' => $page,
'limit' => $limit,
'total' => count($items)
]);
}
#[Route('/items/{id}', name: 'items_show', methods: ['GET'])]
public function show(Item $item): JsonResponse
{
return $this->json($item);
}
#[Route('/items', name: 'items_create', methods: ['POST'])]
public function create(Request $request): JsonResponse
{
$data = json_decode($request->getContent(), true);
$item = new Item();
$item->setName($data['name'] ?? '');
$item->setDescription($data['description'] ?? null);
$item->setPrice($data['price'] ?? 0);
$errors = $this->validator->validate($item);
if (count($errors) > 0) {
$errorMessages = [];
foreach ($errors as $error) {
$errorMessages[] = $error->getMessage();
}
return $this->json(['errors' => $errorMessages], Response::HTTP_BAD_REQUEST);
}
$this->entityManager->persist($item);
$this->entityManager->flush();
return $this->json($item, Response::HTTP_CREATED);
}
#[Route('/items/{id}', name: 'items_update', methods: ['PUT'])]
public function update(Item $item, Request $request): JsonResponse
{
$data = json_decode($request->getContent(), true);
if (isset($data['name'])) {
$item->setName($data['name']);
}
if (isset($data['description'])) {
$item->setDescription($data['description']);
}
if (isset($data['price'])) {
$item->setPrice($data['price']);
}
$item->setUpdatedAt(new \DateTimeImmutable());
$errors = $this->validator->validate($item);
if (count($errors) > 0) {
$errorMessages = [];
foreach ($errors as $error) {
$errorMessages[] = $error->getMessage();
}
return $this->json(['errors' => $errorMessages], Response::HTTP_BAD_REQUEST);
}
$this->entityManager->flush();
return $this->json($item);
}
#[Route('/items/{id}', name: 'items_delete', methods: ['DELETE'])]
public function delete(Item $item): JsonResponse
{
$this->entityManager->remove($item);
$this->entityManager->flush();
return $this->json(null, Response::HTTP_NO_CONTENT);
}
#[Route('/items/stats', name: 'items_stats', methods: ['GET'])]
public function stats(): JsonResponse
{
$total = count($this->itemRepository->findAll());
return $this->json([
'total_items' => $total,
'timestamp' => time()
]);
}
}

Step 6: Configure Routes

Edit config/routes.yaml:

api:
resource: ../src/Controller/Api/
type: attribute
prefix: /api
app:
resource: ../src/Controller/
type: attribute

Step 7: Create Database Migration

Generate migration from entities:

Terminal window
php bin/console make:migration

Run the migration:

Terminal window
php bin/console doctrine:migrations:migrate

Step 8: Install Frontend Assets (Optional)

For asset management with Webpack Encore:

Terminal window
composer require symfony/webpack-encore-bundle
npm install

Step 9: Build and Test Locally

Terminal window
# Install dependencies
composer install
npm install # if using Webpack Encore
# Create database
php bin/console doctrine:database:create
# Run migrations
php bin/console doctrine:migrations:migrate
# Build assets (if using Webpack Encore)
npm run build
# Start development server
symfony server:start -d
# or
php -S localhost:8000 -t public
# Test the API
curl http://localhost:8000/api/health

Deploying Without a Dockerfile

Klutch.sh uses Nixpacks to automatically detect and build your Symfony application from your source code.

Prepare Your Repository

  1. Initialize a Git repository and commit your code:
Terminal window
git init
git add .
git commit -m "Initial Symfony app commit"
  1. Create a .gitignore file:
/node_modules
/public/bundles/
/public/build/
/var/cache/
/var/log/
/vendor
.env
.env.local
.env.*.local
.DS_Store
*.log
.idea/
.vscode/
  1. Create .env.production for production configuration:
APP_ENV=prod
APP_DEBUG=false
DATABASE_URL="postgresql://user:pass@db-host:5432/symfony_db?serverVersion=15&charset=utf8"
REDIS_URL=redis://redis-host:6379
MESSENGER_TRANSPORT_DSN=redis://redis-host:6379/messages
  1. Ensure project includes:

    • composer.json in root
    • public/index.php as entry point
    • Database migrations in migrations/
    • Proper environment variable templates in .env
  2. Push to GitHub:

Terminal window
git remote add origin https://github.com/YOUR_USERNAME/symfony-app.git
git branch -M main
git push -u origin main

Deploy to Klutch.sh

  1. Log in to Klutch.sh dashboard.

  2. Click “Create a new project” and provide a project name.

  3. Inside your project, click “Create a new app”.

  4. Repository Configuration:

    • Select your GitHub repository containing the Symfony app
    • Select the branch to deploy (typically main)
  5. Traffic Settings:

    • Select “HTTP” as the traffic type
  6. Port Configuration:

    • Set the internal port to 8080 (recommended for PHP applications)
  7. Environment Variables: Set the following environment variables in the Klutch.sh dashboard:

    • APP_ENV: Set to prod for production
    • APP_DEBUG: Set to false for production
    • DATABASE_URL: Your PostgreSQL or MySQL connection string
    • REDIS_URL: Your Redis connection URL (if using caching)
    • MESSENGER_TRANSPORT_DSN: Your message broker URL (if using queues)
    • APP_SECRET: A strong random secret key
  8. Build and Start Commands (Optional): If you need to customize the build or start command, set these environment variables:

    • BUILD_COMMAND: Default runs composer install && php bin/console doctrine:migrations:migrate
    • START_COMMAND: Default is php -S 0.0.0.0:8080 -t public
  9. Region, Compute, and Instances:

    • Choose your desired region for optimal latency
    • Select compute resources (Starter for prototypes, Pro/Premium for production)
    • Set the number of instances (start with 1-2, scale as needed)
  10. Click “Create” to deploy. Klutch.sh will automatically build your application and deploy it.

  11. Once deployment completes, your app will be accessible at example-app.klutch.sh.

Verifying the Deployment

Test your deployed app:

Terminal window
curl https://example-app.klutch.sh/api/health

You should receive:

{
"status": "healthy",
"service": "symfony-app"
}

Access the API:

https://example-app.klutch.sh/api/items

Deploying With a Dockerfile

If you prefer more control over your build environment, you can provide a custom Dockerfile. Klutch.sh automatically detects and uses a Dockerfile in your repository’s root directory.

Create a Multi-Stage Dockerfile

Create a Dockerfile in your project root:

# Build stage
FROM composer:2.6 AS builder
WORKDIR /app
# Copy composer files
COPY composer.json composer.lock ./
# Install PHP dependencies
RUN composer install --no-dev --optimize-autoloader
# Frontend build stage (if using Webpack Encore)
FROM node:18-alpine AS node-builder
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm install
COPY . .
RUN npm run build
# Runtime stage
FROM php:8.2-fpm-alpine
WORKDIR /app
# Install required PHP extensions and tools
RUN apk add --no-cache \
postgresql-dev \
mysql-dev \
nginx \
curl \
supervisor
RUN docker-php-ext-install \
pdo \
pdo_mysql \
pdo_pgsql \
mysqli \
intl \
opcache
# Copy application files
COPY --from=builder /app/vendor ./vendor
COPY --from=node-builder /app/public ./public
COPY --chown=www-data:www-data . .
# Create cache and log directories
RUN mkdir -p var/cache var/log && \
chown -R www-data:www-data var public
# Configure Nginx
RUN echo "server { \
listen 8080; \
server_name _; \
root /app/public; \
index index.php; \
client_max_body_size 100M; \
location / { \
try_files \\\$uri /index.php?\\\$args; \
} \
location ~ \.php\$ { \
fastcgi_pass 127.0.0.1:9000; \
fastcgi_index index.php; \
include fastcgi_params; \
fastcgi_param SCRIPT_FILENAME \\\$document_root\\\$fastcgi_script_name; \
} \
}" > /etc/nginx/conf.d/default.conf
# Health check
HEALTHCHECK --interval=30s --timeout=10s --start-period=30s --retries=3 \
CMD curl -f http://localhost:8080/api/health || exit 1
# Expose port
EXPOSE 8080
# Create non-root user
RUN addgroup -g 1000 symfony && \
adduser -D -u 1000 -G symfony symfony
USER symfony
# Start services
CMD ["sh", "-c", "php-fpm & nginx -g 'daemon off;'"]

Deploy the Dockerfile Version

  1. Push your code with the Dockerfile to GitHub:
Terminal window
git add Dockerfile
git commit -m "Add Dockerfile for custom build"
git push
  1. Log in to Klutch.sh dashboard.

  2. Create a new app:

    • Select your GitHub repository and branch
    • Set traffic type to “HTTP”
    • Set the internal port to 8080
    • Add environment variables
    • Click “Create”
  3. Klutch.sh will automatically detect your Dockerfile and use it for building and deployment.


Database Configuration

PostgreSQL Setup

PostgreSQL is recommended for Symfony applications. Configure in .env:

DATABASE_URL="postgresql://user:password@host:5432/symfony_db?serverVersion=15&charset=utf8"

MySQL Setup

Configure MySQL in .env:

DATABASE_URL="mysql://user:password@host:3306/symfony_db?serverVersion=8.0&charset=utf8mb4"

Running Migrations

Create a migration from entities:

Terminal window
php bin/console make:migration

Run migrations during deployment or manually:

Terminal window
php bin/console doctrine:migrations:migrate

Security Configuration

Authentication Setup

Install security bundle:

Terminal window
composer require symfony/security-bundle

Configure in config/packages/security.yaml:

security:
password_hashers:
Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface:
algorithm: auto
providers:
app_user_provider:
entity:
class: App\Entity\User
property: email
firewalls:
dev:
pattern: ^/(_(profiler|wdt)|css|images|js)/
security: false
main:
lazy: true
provider: app_user_provider
custom_authenticators:
- App\Security\ApiTokenAuthenticator
access_control:
- { path: ^/api, roles: IS_AUTHENTICATED }
- { path: ^/api/health, roles: PUBLIC_ACCESS }

Caching Configuration

Redis Setup

Configure Redis in config/packages/cache.yaml:

framework:
cache:
default: cache.adapter.redis
pools:
cache.adapter.redis:
adapter: cache.adapter.redis
public: false
tags: true

Configure in .env:

REDIS_URL=redis://127.0.0.1:6379

Using Cache

use Symfony\Contracts\Cache\CacheInterface;
public function getCachedData(CacheInterface $cache)
{
return $cache->get('items_cache', function (ItemInterface $item) {
$item->expiresAfter(3600);
return $this->fetchItems();
});
}

Messenger and Async Processing

Setup Messenger

Install messenger:

Terminal window
composer require symfony/messenger

Configure in .env:

MESSENGER_TRANSPORT_DSN=redis://127.0.0.1:6379/messages

Create a message:

Terminal window
php bin/console make:message ProcessItem

Create a handler:

Terminal window
php bin/console make:message-handler ProcessItemMessageHandler

Dispatch a message:

use App\Message\ProcessItem;
use Symfony\Component\Messenger\MessageBusInterface;
public function create(MessageBusInterface $bus)
{
$bus->dispatch(new ProcessItem($item));
}

Logging and Monitoring

Configure Logging

Edit config/packages/monolog.yaml:

monolog:
handlers:
main:
type: rotating_file
path: "%kernel.logs_dir%/%kernel.environment%.log"
max_files: 10
level: info
console:
type: console
process_psr_3_messages: false

Using Logging

use Psr\Log\LoggerInterface;
public function process(LoggerInterface $logger)
{
$logger->info('Processing item', ['id' => $item->getId()]);
$logger->error('Error occurred', ['error' => $exception->getMessage()]);
}

Persistent Storage for Logs and Uploads

Adding Persistent Volume

  1. In the Klutch.sh app dashboard, navigate to “Persistent Storage” or “Volumes”
  2. Click “Add Volume”
  3. Set the mount path: /app/var (for logs and cache)
  4. Set the size based on your needs (e.g., 10 GB)
  5. Save and redeploy

Configure Log Path

Configure logging to use persistent storage:

monolog:
handlers:
main:
type: rotating_file
path: "/app/var/log/%kernel.environment%.log"

Custom Domains

To serve your Symfony application from a custom domain:

  1. In the Klutch.sh app dashboard, navigate to “Custom Domains”
  2. Click “Add Custom Domain”
  3. Enter your domain (e.g., api.example.com)
  4. Follow the DNS configuration instructions provided

Example DNS configuration:

api.example.com CNAME example-app.klutch.sh

Update APP_URL environment variable if needed:

APP_URL=https://api.example.com

Troubleshooting

Issue 1: Database Migration Fails

Problem: Migrations fail during deployment with connection errors.

Solution:

  • Verify DATABASE_URL is correctly formatted
  • Ensure database server is running and accessible
  • Check database user has CREATE/ALTER TABLE permissions
  • Review migration file syntax for errors
  • Test connection string format with database client
  • Check firewall rules allow the connection

Issue 2: Cache or Session Errors

Problem: Cache operations fail or sessions are not persisting.

Solution:

  • Verify REDIS_URL is correctly configured
  • Ensure Redis server is running and accessible
  • Check Redis connection string format
  • Verify firewall rules allow Redis connection
  • Monitor Redis server logs for errors

Issue 3: Slow Application Performance

Problem: Application responds slowly to requests.

Solution:

  • Enable caching for frequently accessed data
  • Optimize database queries and add indexes
  • Use eager loading in Doctrine queries
  • Enable HTTP caching headers
  • Monitor performance metrics in dashboard
  • Consider scaling to additional instances

Issue 4: File Upload Permission Denied

Problem: File uploads fail with permission errors.

Solution:

  • Mount persistent storage for upload directory
  • Ensure web server user has write permissions
  • Check disk space availability
  • Verify file size limits in php.ini
  • Review upload directory permissions

Issue 5: Memory or Disk Space Issues

Problem: Application crashes due to resource limits.

Solution:

  • Increase instance compute resources
  • Increase persistent storage size
  • Optimize database queries
  • Clean up old cache files
  • Monitor resource usage via dashboard

Best Practices for Production Deployment

  1. Environment Configuration: Use .env for sensitive data

    APP_ENV=prod
    APP_SECRET={STRONG_SECRET}
    DATABASE_PASSWORD={STRONG_PASSWORD}
  2. Enable Caching: Use Redis for performance

    framework:
    cache:
    default: cache.adapter.redis
  3. Database Optimization: Add indexes and optimize queries

    $query = $this->itemRepository->findBySearch($search);
  4. Input Validation: Always validate user input

    $errors = $this->validator->validate($item);
  5. Error Handling: Don’t expose sensitive information

    if (!$item) {
    throw $this->createNotFoundException('Item not found');
    }
  6. Security: Enable HTTPS and security headers

    framework:
    http_method_override: false
  7. Logging: Log important events and errors

    $logger->info('Item created', ['id' => $item->getId()]);
  8. Async Processing: Use messenger for long-running tasks

    $bus->dispatch(new ProcessItem($item));
  9. Rate Limiting: Implement rate limiting for APIs

    framework:
    rate_limiter:
    api:
    policy: 'sliding_window'
    limit: 100
    interval: '1 minute'
  10. Monitoring: Configure health checks

    if ($item->getId() > 0) {
    return $this->json(['status' => 'ok']);
    }

Resources


Conclusion

Deploying Symfony applications to Klutch.sh provides a robust, scalable platform for building and running enterprise-grade PHP applications. Symfony’s flexibility, comprehensive ecosystem, and best-in-class tools make it ideal for complex, mission-critical systems.

Key takeaways:

  • Use Nixpacks for quick deployments with automatic PHP detection
  • Use Docker for complete control over PHP extensions and dependencies
  • Leverage Doctrine ORM for efficient database operations
  • Implement proper security with authentication and authorization
  • Use caching with Redis for improved performance
  • Configure persistent storage for logs and application data
  • Enable HTTPS and security best practices in production
  • Implement structured logging for monitoring and debugging
  • Use messenger for async processing of long-running tasks
  • Monitor application health and performance metrics

For additional help, refer to the Symfony documentation or Klutch.sh support resources.