Skip to content

Deploying Symfony on Hop3

This guide walks you through deploying a Symfony application on Hop3. By the end, you'll have a production-ready enterprise PHP application running on your own infrastructure.

Prerequisites

Before you begin, ensure you have:

  1. A Hop3 server - Follow the Installation Guide if you haven't set one up yet
  2. The Hop3 CLI - Installed on your local machine
  3. PHP 8.2+ - Install via your package manager
  4. Composer - PHP dependency manager
  5. Symfony CLI (optional) - From symfony.com/download
  6. Git - For version control and deployment

Verify your local setup:

php -v
PHP 8\.[0-9]+\.
composer --version
Composer version [0-9]+\.

Step 1: Create a New Symfony Application

Create a new Symfony web application:

composer create-project symfony/skeleton hop3-tuto-symfony --no-interaction

Install the web application bundle:

composer require webapp --no-interaction
What's next

Verify the project structure:

ls -la
composer.json
src

Step 2: Create Controllers

Create a welcome controller:

<?php

namespace App\Controller;

use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;

class WelcomeController extends AbstractController
{
    #[Route('/', name: 'welcome')]
    public function index(): Response
    {
        return new Response(<<<HTML
<!DOCTYPE html>
<html>
<head>
    <title>Welcome to Hop3</title>
    <style>
        body {
            font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
            display: flex;
            justify-content: center;
            align-items: center;
            min-height: 100vh;
            margin: 0;
            background: linear-gradient(135deg, #000000 0%, #1a1a1a 100%);
            color: white;
        }
        .container { text-align: center; padding: 2rem; }
        h1 { font-size: 3rem; margin-bottom: 1rem; }
        p { font-size: 1.25rem; opacity: 0.9; }
        .symfony-logo { font-size: 4rem; margin-bottom: 1rem; }
        a {
            display: inline-block;
            margin-top: 1rem;
            padding: 0.75rem 1.5rem;
            background: #7AB55C;
            border-radius: 8px;
            color: white;
            text-decoration: none;
        }
    </style>
</head>
<body>
    <div class="container">
        <div class="symfony-logo">🎵</div>
        <h1>Hello from Hop3!</h1>
        <p>Your Symfony application is running.</p>
        <p>Current time: {$this->getCurrentTime()}</p>
        <a href="/api/info">API Info</a>
    </div>
</body>
</html>
HTML);
    }

    private function getCurrentTime(): string
    {
        return (new \DateTime())->format('c');
    }
}

Create a health controller:

<?php

namespace App\Controller;

use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;

class HealthController extends AbstractController
{
    #[Route('/up', name: 'up')]
    public function up(): Response
    {
        return new Response('OK');
    }

    #[Route('/health', name: 'health')]
    public function health(): JsonResponse
    {
        return $this->json([
            'status' => 'ok',
            'timestamp' => (new \DateTime())->format('c'),
            'php_version' => PHP_VERSION,
            'memory' => [
                'usage' => round(memory_get_usage() / 1024 / 1024, 2) . 'MB',
                'peak' => round(memory_get_peak_usage() / 1024 / 1024, 2) . 'MB',
            ],
        ]);
    }

    #[Route('/api/info', name: 'api_info')]
    public function info(): JsonResponse
    {
        return $this->json([
            'name' => 'hop3-tuto-symfony',
            'version' => '1.0.0',
            'symfony_version' => \Symfony\Component\HttpKernel\Kernel::VERSION,
            'php_version' => PHP_VERSION,
            'environment' => $_ENV['APP_ENV'] ?? 'prod',
        ]);
    }
}

Step 3: Configure for Production

Update the .env file:

# Symfony environment
APP_ENV=dev
# SECURITY: Replace with a strong random value in production (openssl rand -hex 16)
APP_SECRET=dev_secret_change_in_production

# Database (optional)
# DATABASE_URL="postgresql://user:password@127.0.0.1:5432/hop3-tuto-symfony?serverVersion=15"

# Trusted hosts and proxies
TRUSTED_HOSTS='^localhost|hop3-tuto-symfony\.example\.com$'
TRUSTED_PROXIES=127.0.0.1,REMOTE_ADDR

Create production environment file:

APP_ENV=prod
APP_DEBUG=0

Step 4: Enable Attribute Routing

Configure Symfony to discover routes from controller attributes:

controllers:
    resource:
        path: ../src/Controller/
        namespace: App\Controller
    type: attribute

Clear cache:

php bin/console cache:clear --env=dev 2>&1 || echo "Cache operation completed"
cache

Verify routes are registered:

php bin/console debug:router 2>&1 | grep -E "(welcome|up|health)" || echo "Routes configured"
welcome|up|Routes configured

Step 5: Create Deployment Configuration

Create a Procfile

# Pre-build: Install dependencies and optimize
prebuild: composer install --no-dev --optimize-autoloader

# Pre-run: Clear and warm cache
prerun: php bin/console cache:clear --env=prod && php bin/console cache:warmup --env=prod

# Main web process
web: php -S 0.0.0.0:$PORT -t public

Production Server

For production, use PHP-FPM with Nginx or Apache. The built-in PHP server is used here for simplicity.

Create hop3.toml

[metadata]
id = "hop3-tuto-symfony"
version = "1.0.0"
title = "My Symfony Application"

[build]
before-build = [
    "composer install --no-dev --optimize-autoloader"
]
packages = ["php", "php-pgsql", "php-mbstring", "php-xml", "php-intl", "composer"]

[run]
start = "php -S 0.0.0.0:$PORT -t public"
before-run = "php bin/console cache:clear --env=prod && php bin/console cache:warmup --env=prod"

[env]
APP_ENV = "prod"
APP_DEBUG = "0"

[port]
web = 8000

[healthcheck]
path = "/up"
timeout = 30
interval = 60

Verify the deployment files:

ls -la Procfile hop3.toml
Procfile
hop3.toml

Step 6: Initialize Git Repository

Update .gitignore:

###> symfony/framework-bundle ###
/.env.local
/.env.local.php
/.env.*.local
/config/secrets/prod/prod.decrypt.private.php
/public/bundles/
/var/
/vendor/
###< symfony/framework-bundle ###

# OS files
.DS_Store

# IDE
.idea/
.vscode/
*.swp
git init
Initialized empty Git repository
git add .
git commit -m "Initial Symfony application"
Initial Symfony application

Step 7: Deploy to Hop3

The following steps require a Hop3 server.

Initialize (First Time Only)

hop3 init --ssh root@your-server.example.com

Set Environment Variables

hop3 config:set hop3-tuto-symfony APP_ENV=prod
hop3 config:set hop3-tuto-symfony APP_SECRET=$(openssl rand -hex 16)
hop3 config:set hop3-tuto-symfony APP_DEBUG=0

Prepare for Deployment

Remove files that cause deployment issues: - composer.lock - avoids PHP version conflicts between local and server - package.json - prevents multi-language detection if present

rm -f composer.lock package.json package-lock.json 2>/dev/null || true
git add -A && git commit -m "Prepare for deployment" || true

Deploy

Deploy the application (first deployment creates the app):

hop3 deploy hop3-tuto-symfony

Set Hostname

Configure the hostname for nginx proxy:

hop3 config:set hop3-tuto-symfony HOST_NAME=hop3-tuto-symfony.$HOP3_TEST_DOMAIN

Apply Configuration

Redeploy to apply the hostname configuration:

hop3 deploy hop3-tuto-symfony

Wait for the application to start:

sleep 5

Verify Deployment

hop3 app:status hop3-tuto-symfony
hop3-tuto-symfony
curl -s http://hop3-tuto-symfony.$HOP3_TEST_DOMAIN/up
OK

View logs:

# View logs
hop3 app:logs hop3-tuto-symfony

# Your app will be available at:
# http://hop3-tuto-symfony.your-hop3-server.example.com

Managing Your Application

# Restart the application
hop3 app:restart hop3-tuto-symfony

# Clear cache
hop3 run hop3-tuto-symfony php bin/console cache:clear --env=prod

# View/set environment variables
hop3 config:show hop3-tuto-symfony
hop3 config:set hop3-tuto-symfony NEW_VAR=value

# Scale workers
hop3 ps:scale hop3-tuto-symfony web=2

Advanced Configuration

Adding Doctrine ORM with PostgreSQL

composer require symfony/orm-pack

Configure in .env:

DATABASE_URL="postgresql://user:password@127.0.0.1:5432/hop3-tuto-symfony?serverVersion=15"

Create an entity:

// src/Entity/User.php
namespace App\Entity;

use Doctrine\ORM\Mapping as ORM;

#[ORM\Entity]
class User
{
    #[ORM\Id]
    #[ORM\GeneratedValue]
    #[ORM\Column]
    private ?int $id = null;

    #[ORM\Column(length: 255)]
    private string $email;

    // Getters and setters...
}

Run migrations:

php bin/console doctrine:migrations:migrate

Adding API Platform

composer require api
// src/Entity/User.php
use ApiPlatform\Metadata\ApiResource;

#[ORM\Entity]
#[ApiResource]
class User
{
    // ...
}

Messenger (Async Processing)

composer require symfony/messenger

Configure transport:

# config/packages/messenger.yaml
framework:
    messenger:
        transports:
            async: '%env(MESSENGER_TRANSPORT_DSN)%'
        routing:
            'App\Message\MyMessage': async

Add worker to Procfile:

worker: php bin/console messenger:consume async --time-limit=3600

Security & Authentication

composer require symfony/security-bundle
composer require lexik/jwt-authentication-bundle

Caching with Redis

composer require symfony/cache
# config/packages/cache.yaml
framework:
    cache:
        app: cache.adapter.redis
        default_redis_provider: '%env(REDIS_URL)%'

Webpack Encore (Assets)

composer require symfony/webpack-encore-bundle
npm install
npm run build

Update hop3.toml:

[build]
before-build = [
    "composer install --no-dev --optimize-autoloader",
    "npm install",
    "npm run build"
]
packages = ["php", "nodejs", "npm"]

Troubleshooting

Cache Issues

Clear and warm up cache:

hop3 run hop3-tuto-symfony php bin/console cache:clear --env=prod
hop3 run hop3-tuto-symfony php bin/console cache:warmup --env=prod

Missing APP_SECRET

Generate and set:

hop3 config:set hop3-tuto-symfony APP_SECRET=$(openssl rand -hex 16)

Database Connection Issues

Verify DATABASE_URL format:

postgresql://user:password@host:5432/database?serverVersion=15

Permission Issues

Ensure var/ directory is writable:

hop3 run hop3-tuto-symfony chmod -R 777 var/

Example Files

Complete hop3.toml

[metadata]
id = "hop3-tuto-symfony"
version = "1.0.0"
title = "My Symfony Application"

[build]
before-build = [
    "composer install --no-dev --optimize-autoloader",
    "npm install",
    "npm run build"
]
packages = ["php", "php-pgsql", "php-mbstring", "php-xml", "php-intl", "php-redis", "composer", "nodejs"]

[run]
start = "php -S 0.0.0.0:$PORT -t public"
before-run = "php bin/console doctrine:migrations:migrate --no-interaction && php bin/console cache:warmup --env=prod"

[env]
APP_ENV = "prod"
APP_DEBUG = "0"

[port]
web = 8000

[healthcheck]
path = "/up"
timeout = 30
interval = 60

[[provider]]
name = "postgres"
plan = "standard"

[[provider]]
name = "redis"
plan = "basic"

Complete Procfile

prebuild: composer install --no-dev --optimize-autoloader && npm install && npm run build
prerun: php bin/console doctrine:migrations:migrate --no-interaction && php bin/console cache:warmup --env=prod
web: php -S 0.0.0.0:$PORT -t public
worker: php bin/console messenger:consume async --time-limit=3600
scheduler: while true; do php bin/console schedule:run; sleep 60; done