···11+# RSSBase application Dagger module
22+33+This module is written in [Dang](https://github.com/vito/dang) and uses the Dagger CLI packaged by this repo's devenv.
44+55+The application module is configured by `application/dagger-module.toml`; the repository root `dagger.toml` installs it as the `application` workspace module.
66+77+From the repository root or `application/`:
88+99+```bash
1010+dagger functions
1111+dagger check --list
1212+dagger check
1313+dagger call application php-version
1414+dagger call application composer-validate
1515+dagger call application build-prod sync
1616+dagger call application console --args about
1717+```
···11+# In all environments, the following files are loaded if they exist,
22+# the latter taking precedence over the former:
33+#
44+# * .env contains default values for the environment variables needed by the app
55+# * .env.local uncommitted file with local overrides
66+# * .env.$APP_ENV committed environment-specific defaults
77+# * .env.$APP_ENV.local uncommitted environment-specific overrides
88+#
99+# Real environment variables win over .env files.
1010+#
1111+# DO NOT DEFINE PRODUCTION SECRETS IN THIS FILE NOR IN ANY OTHER COMMITTED FILES.
1212+# https://symfony.com/doc/current/configuration/secrets.html
1313+#
1414+# Run "composer dump-env prod" to compile .env files for production use (requires symfony/flex >=1.2).
1515+# https://symfony.com/doc/current/best_practices.html#use-environment-variables-for-infrastructure-configuration
1616+1717+###> symfony/framework-bundle ###
1818+APP_ENV=dev
1919+APP_SECRET=
2020+APP_SHARE_DIR=var/share
2121+###< symfony/framework-bundle ###
2222+2323+###> symfony/routing ###
2424+# Configure how to generate URLs in non-HTTP contexts, such as CLI commands.
2525+# See https://symfony.com/doc/current/routing.html#generating-urls-in-commands
2626+DEFAULT_URI=http://localhost
2727+###< symfony/routing ###
···11+* text=auto eol=lf
22+33+*.conf text eol=lf
44+*.html text eol=lf
55+*.ini text eol=lf
66+*.js text eol=lf
77+*.json text eol=lf
88+*.md text eol=lf
99+*.php text eol=lf
1010+*.sh text eol=lf
1111+*.yaml text eol=lf
1212+*.yml text eol=lf
1313+bin/console text eol=lf
1414+composer.lock text eol=lf merge=ours
1515+1616+*.ico binary
1717+*.png binary
···11+# Application using Symfony
22+33+## Getting Started
44+55+1. Run `docker compose up --build --wait` to set up and start a fresh Symfony project
66+2. Open `https://localhost` in your favorite web browser and [accept the auto-generated TLS certificate](https://stackoverflow.com/a/15076602/1352334)
77+88+99+## Features
1010+1111+- Production, development and CI ready
1212+- Just 1 service by default
1313+- Super-readable configuration
1414+- Blazing-fast performance thanks to [the worker mode of FrankenPHP](https://frankenphp.dev/docs/worker/)
1515+- [Installation of extra Docker Compose services](docs/extra-services.md) with Symfony Flex
1616+- Automatic HTTPS (in dev and prod)
1717+- HTTP/3 and [Early Hints](https://symfony.com/blog/new-in-symfony-6-3-early-hints) support
1818+- Real-time messaging thanks to a built-in [Mercure hub](https://symfony.com/doc/current/mercure.html)
1919+- [Vulcain](https://vulcain.rocks) support
2020+- Native [XDebug](docs/xdebug.md) integration
2121+- [Hot Reloading](https://frankenphp.dev/docs/hot-reload/)
2222+- [Dev Container](https://containers.dev/) support
2323+- [AI coding agents](docs/agents.md) with an optional network sandbox
2424+- Rootless, slim production image
2525+2626+## Docs
2727+2828+1. [Options available](docs/options.md)
2929+2. [Using Symfony Docker with an existing project](docs/existing-project.md)
3030+3. [Support for extra services](docs/extra-services.md)
3131+4. [Deploying in production](docs/production.md)
3232+5. [Debugging with Xdebug](docs/xdebug.md)
3333+6. [TLS Certificates](docs/tls.md)
3434+7. [Using MySQL instead of PostgreSQL](docs/mysql.md)
3535+8. [Using Alpine Linux instead of Debian](docs/alpine.md)
3636+9. [Using a Makefile](docs/makefile.md)
3737+10. [Updating the template](docs/updating.md)
3838+11. [Troubleshooting](docs/troubleshooting.md)
3939+12. [Using AI coding agents](docs/agents.md)
+21
application/bin/console
···11+#!/usr/bin/env php
22+<?php
33+44+use App\Kernel;
55+use Symfony\Bundle\FrameworkBundle\Console\Application;
66+77+if (!is_dir(dirname(__DIR__).'/vendor')) {
88+ throw new LogicException('Dependencies are missing. Try running "composer install".');
99+}
1010+1111+if (!is_file(dirname(__DIR__).'/vendor/autoload_runtime.php')) {
1212+ throw new LogicException('Symfony Runtime is missing. Try running "composer require symfony/runtime".');
1313+}
1414+1515+require_once dirname(__DIR__).'/vendor/autoload_runtime.php';
1616+1717+return function (array $context) {
1818+ $kernel = new Kernel($context['APP_ENV'], (bool) $context['APP_DEBUG']);
1919+2020+ return new Application($kernel);
2121+};
+30
application/compose.override.yaml
···11+---
22+# Development environment override
33+services:
44+ php:
55+ image: ${IMAGES_PREFIX:-}app-php-dev
66+ build:
77+ context: .
88+ target: frankenphp_dev
99+ volumes:
1010+ - ./:/app
1111+ - ./frankenphp/Caddyfile:/etc/frankenphp/Caddyfile:ro
1212+ - ./frankenphp/conf.d/20-app.dev.ini:/usr/local/etc/php/app.conf.d/20-app.dev.ini:ro
1313+ # Keep var/ off the bind-mount for faster I/O on Mac/Windows; comment to inspect from the host.
1414+ - /app/var
1515+ # If you develop on Mac or Windows you can remove the vendor/ directory
1616+ # from the bind-mount for better performance by enabling the next line:
1717+ #- /app/vendor
1818+ environment:
1919+ FRANKENPHP_WORKER_CONFIG: watch
2020+ FRANKENPHP_SITE_CONFIG: hot_reload
2121+ MERCURE_EXTRA_DIRECTIVES: demo
2222+ # See https://xdebug.org/docs/all_settings#mode
2323+ XDEBUG_MODE: "${XDEBUG_MODE:-develop}"
2424+ APP_ENV: "${APP_ENV:-dev}"
2525+ extra_hosts:
2626+ # Ensure that host.docker.internal is correctly defined on Linux
2727+ - host.docker.internal:host-gateway
2828+ tty: true
2929+###> symfony/mercure-bundle ###
3030+###< symfony/mercure-bundle ###
···11+framework:
22+ cache:
33+ # Unique name of your app: used to compute stable namespaces for cache keys.
44+ #prefix_seed: your_vendor_name/app_name
55+66+ # The "app" cache stores to the filesystem by default.
77+ # The data in this cache should persist between deploys.
88+ # Other options include:
99+1010+ # Redis
1111+ #app: cache.adapter.redis
1212+ #default_redis_provider: redis://localhost
1313+1414+ # APCu (not recommended with heavy random-write workloads as memory fragmentation can cause perf issues)
1515+ #app: cache.adapter.apcu
1616+1717+ # Namespaced pools use the above "app" backend by default
1818+ #pools:
1919+ #my.dedicated.cache: null
+15
application/config/packages/framework.yaml
···11+# see https://symfony.com/doc/current/reference/configuration/framework.html
22+framework:
33+ secret: '%env(APP_SECRET)%'
44+55+ # Note that the session will be started ONLY if you read or write from it.
66+ session: true
77+88+ #esi: true
99+ #fragments: true
1010+1111+when@test:
1212+ framework:
1313+ test: true
1414+ session:
1515+ storage_factory_id: session.storage.factory.mock_file
+10
application/config/packages/routing.yaml
···11+framework:
22+ router:
33+ # Configure how to generate URLs in non-HTTP contexts, such as CLI commands.
44+ # See https://symfony.com/doc/current/routing.html#generating-urls-in-commands
55+ default_uri: '%env(DEFAULT_URI)%'
66+77+when@prod:
88+ framework:
99+ router:
1010+ strict_requirements: null
···11+<?php
22+33+// This file is auto-generated and is for apps only. Bundles SHOULD NOT rely on its content.
44+55+namespace Symfony\Component\DependencyInjection\Loader\Configurator;
66+77+use Symfony\Component\Config\Loader\ParamConfigurator as Param;
88+99+/**
1010+ * This class provides array-shapes for configuring the services and bundles of an application.
1111+ *
1212+ * Services declared with the config() method below are autowired and autoconfigured by default.
1313+ *
1414+ * This is for apps only. Bundles SHOULD NOT use it.
1515+ *
1616+ * Example:
1717+ *
1818+ * ```php
1919+ * // config/services.php
2020+ * namespace Symfony\Component\DependencyInjection\Loader\Configurator;
2121+ *
2222+ * return App::config([
2323+ * 'services' => [
2424+ * 'App\\' => [
2525+ * 'resource' => '../src/',
2626+ * ],
2727+ * ],
2828+ * ]);
2929+ * ```
3030+ *
3131+ * @psalm-type ImportsConfig = list<string|array{
3232+ * resource: string,
3333+ * type?: string|null,
3434+ * ignore_errors?: bool,
3535+ * }>
3636+ * @psalm-type ParametersConfig = array<string, scalar|\UnitEnum|array<scalar|\UnitEnum|array<mixed>|Param|null>|Param|null>
3737+ * @psalm-type ArgumentsType = list<mixed>|array<string, mixed>
3838+ * @psalm-type CallType = array<string, ArgumentsType>|array{0:string, 1?:ArgumentsType, 2?:bool}|array{method:string, arguments?:ArgumentsType, returns_clone?:bool}
3939+ * @psalm-type TagsType = list<string|array<string, array<string, mixed>>> // arrays inside the list must have only one element, with the tag name as the key
4040+ * @psalm-type CallbackType = string|array{0:string|ReferenceConfigurator,1:string}|\Closure|ReferenceConfigurator
4141+ * @psalm-type DeprecationType = array{package: string, version: string, message?: string}
4242+ * @psalm-type DefaultsType = array{
4343+ * public?: bool,
4444+ * tags?: TagsType,
4545+ * resource_tags?: TagsType,
4646+ * autowire?: bool,
4747+ * autoconfigure?: bool,
4848+ * bind?: array<string, mixed>,
4949+ * }
5050+ * @psalm-type InstanceofType = array{
5151+ * shared?: bool,
5252+ * lazy?: bool|string,
5353+ * public?: bool,
5454+ * properties?: array<string, mixed>,
5555+ * configurator?: CallbackType,
5656+ * calls?: list<CallType>,
5757+ * tags?: TagsType,
5858+ * resource_tags?: TagsType,
5959+ * autowire?: bool,
6060+ * bind?: array<string, mixed>,
6161+ * constructor?: string,
6262+ * }
6363+ * @psalm-type DefinitionType = array{
6464+ * class?: string,
6565+ * file?: string,
6666+ * parent?: string,
6767+ * shared?: bool,
6868+ * synthetic?: bool,
6969+ * lazy?: bool|string,
7070+ * public?: bool,
7171+ * abstract?: bool,
7272+ * deprecated?: DeprecationType,
7373+ * factory?: CallbackType,
7474+ * configurator?: CallbackType,
7575+ * arguments?: ArgumentsType,
7676+ * properties?: array<string, mixed>,
7777+ * calls?: list<CallType>,
7878+ * tags?: TagsType,
7979+ * resource_tags?: TagsType,
8080+ * decorates?: string,
8181+ * decorates_tag?: string,
8282+ * decoration_inner_name?: string,
8383+ * decoration_priority?: int,
8484+ * decoration_on_invalid?: 'exception'|'ignore'|null,
8585+ * autowire?: bool,
8686+ * autoconfigure?: bool,
8787+ * bind?: array<string, mixed>,
8888+ * constructor?: string,
8989+ * from_callable?: CallbackType,
9090+ * }
9191+ * @psalm-type AliasType = string|array{
9292+ * alias: string,
9393+ * public?: bool,
9494+ * deprecated?: DeprecationType,
9595+ * }
9696+ * @psalm-type PrototypeType = array{
9797+ * resource: string,
9898+ * namespace?: string,
9999+ * exclude?: string|list<string>,
100100+ * parent?: string,
101101+ * shared?: bool,
102102+ * lazy?: bool|string,
103103+ * public?: bool,
104104+ * abstract?: bool,
105105+ * deprecated?: DeprecationType,
106106+ * factory?: CallbackType,
107107+ * arguments?: ArgumentsType,
108108+ * properties?: array<string, mixed>,
109109+ * configurator?: CallbackType,
110110+ * calls?: list<CallType>,
111111+ * tags?: TagsType,
112112+ * resource_tags?: TagsType,
113113+ * autowire?: bool,
114114+ * autoconfigure?: bool,
115115+ * bind?: array<string, mixed>,
116116+ * constructor?: string,
117117+ * }
118118+ * @psalm-type StackType = array{
119119+ * stack: list<DefinitionType|AliasType|PrototypeType|array<class-string, ArgumentsType|null>>,
120120+ * public?: bool,
121121+ * deprecated?: DeprecationType,
122122+ * decorates?: string,
123123+ * decorates_tag?: string,
124124+ * decoration_inner_name?: string,
125125+ * decoration_priority?: int,
126126+ * decoration_on_invalid?: 'exception'|'ignore'|null,
127127+ * }
128128+ * @psalm-type ServicesConfig = array{
129129+ * _defaults?: DefaultsType,
130130+ * _instanceof?: array<class-string, InstanceofType>,
131131+ * ...<string, DefinitionType|AliasType|PrototypeType|StackType|ArgumentsType|null>
132132+ * }
133133+ * @psalm-type ExtensionType = array<string, mixed>
134134+ * @psalm-type FrameworkConfig = array{
135135+ * secret?: scalar|Param|null,
136136+ * http_method_override?: bool|Param, // Set true to enable support for the '_method' request parameter to determine the intended HTTP method on POST requests. // Default: false
137137+ * allowed_http_method_override?: null|list<string|Param>,
138138+ * trust_x_sendfile_type_header?: scalar|Param|null, // Set true to enable support for xsendfile in binary file responses. // Default: "%env(bool:default::SYMFONY_TRUST_X_SENDFILE_TYPE_HEADER)%"
139139+ * ide?: scalar|Param|null, // Default: "%env(default::SYMFONY_IDE)%"
140140+ * test?: bool|Param,
141141+ * default_locale?: scalar|Param|null, // Default: "en"
142142+ * set_locale_from_accept_language?: bool|Param, // Whether to use the Accept-Language HTTP header to set the Request locale (only when the "_locale" request attribute is not passed). // Default: false
143143+ * set_content_language_from_locale?: bool|Param, // Whether to set the Content-Language HTTP header on the Response using the Request locale. // Default: false
144144+ * enabled_locales?: list<scalar|Param|null>,
145145+ * trusted_hosts?: string|list<scalar|Param|null>,
146146+ * trusted_proxies?: mixed, // Default: ["%env(default::SYMFONY_TRUSTED_PROXIES)%"]
147147+ * trusted_headers?: string|list<scalar|Param|null>,
148148+ * error_controller?: scalar|Param|null, // Default: "error_controller"
149149+ * handle_all_throwables?: bool|Param, // HttpKernel will handle all kinds of \Throwable. // Default: true
150150+ * csrf_protection?: bool|array{
151151+ * enabled?: scalar|Param|null, // Default: null
152152+ * stateless_token_ids?: list<scalar|Param|null>,
153153+ * check_header?: scalar|Param|null, // Whether to check the CSRF token in a header in addition to a cookie when using stateless protection. // Default: false
154154+ * cookie_name?: scalar|Param|null, // The name of the cookie to use when using stateless protection. // Default: "csrf-token"
155155+ * },
156156+ * form?: bool|array{ // Form configuration
157157+ * enabled?: bool|Param, // Default: false
158158+ * csrf_protection?: bool|array{
159159+ * enabled?: scalar|Param|null, // Default: null
160160+ * token_id?: scalar|Param|null, // Default: null
161161+ * field_name?: scalar|Param|null, // Default: "_token"
162162+ * field_attr?: array<string, scalar|Param|null>,
163163+ * },
164164+ * },
165165+ * http_cache?: bool|array{ // HTTP cache configuration
166166+ * enabled?: bool|Param, // Default: false
167167+ * debug?: bool|Param, // Default: "%kernel.debug%"
168168+ * trace_level?: "none"|"short"|"full"|Param,
169169+ * trace_header?: scalar|Param|null,
170170+ * default_ttl?: int|Param,
171171+ * private_headers?: list<scalar|Param|null>,
172172+ * skip_response_headers?: list<scalar|Param|null>,
173173+ * allow_reload?: bool|Param,
174174+ * allow_revalidate?: bool|Param,
175175+ * stale_while_revalidate?: int|Param,
176176+ * stale_if_error?: int|Param,
177177+ * terminate_on_cache_hit?: bool|Param, // Deprecated: Setting the "framework.http_cache.terminate_on_cache_hit.terminate_on_cache_hit" configuration option is deprecated. It will be removed in version 9.0.
178178+ * },
179179+ * esi?: bool|array{ // ESI configuration
180180+ * enabled?: bool|Param, // Default: false
181181+ * },
182182+ * ssi?: bool|array{ // SSI configuration
183183+ * enabled?: bool|Param, // Default: false
184184+ * },
185185+ * fragments?: bool|array{ // Fragments configuration
186186+ * enabled?: bool|Param, // Default: false
187187+ * hinclude_default_template?: scalar|Param|null, // Default: null
188188+ * path?: scalar|Param|null, // Default: "/_fragment"
189189+ * },
190190+ * profiler?: bool|array{ // Profiler configuration
191191+ * enabled?: bool|Param, // Default: false
192192+ * collect?: bool|Param, // Default: true
193193+ * collect_parameter?: scalar|Param|null, // The name of the parameter to use to enable or disable collection on a per request basis. // Default: null
194194+ * only_exceptions?: bool|Param, // Default: false
195195+ * only_main_requests?: bool|Param, // Default: false
196196+ * dsn?: scalar|Param|null, // Default: "file:%kernel.cache_dir%/profiler"
197197+ * collect_serializer_data?: true|Param, // Deprecated: Setting the "framework.profiler.collect_serializer_data.collect_serializer_data" configuration option is deprecated. It will be removed in version 9.0. // Default: true
198198+ * },
199199+ * workflows?: bool|array{
200200+ * enabled?: bool|Param, // Default: false
201201+ * workflows?: array<string, array{ // Default: []
202202+ * audit_trail?: bool|array{
203203+ * enabled?: bool|Param, // Default: false
204204+ * },
205205+ * type?: "workflow"|"state_machine"|Param, // Default: "state_machine"
206206+ * marking_store?: array{
207207+ * type?: "method"|Param,
208208+ * property?: scalar|Param|null,
209209+ * service?: scalar|Param|null,
210210+ * },
211211+ * supports?: string|list<scalar|Param|null>,
212212+ * definition_validators?: list<scalar|Param|null>,
213213+ * support_strategy?: scalar|Param|null,
214214+ * initial_marking?: \BackedEnum|string|list<scalar|Param|null>,
215215+ * events_to_dispatch?: null|list<string|Param>,
216216+ * places?: string|list<array{ // Default: []
217217+ * name?: scalar|Param|null,
218218+ * metadata?: array<string, mixed>,
219219+ * }>,
220220+ * transitions?: list<array{ // Default: []
221221+ * name?: string|Param,
222222+ * guard?: string|Param, // An expression to block the transition.
223223+ * from?: \BackedEnum|string|list<array{ // Default: []
224224+ * place?: string|Param,
225225+ * weight?: int|Param, // Default: 1
226226+ * }>,
227227+ * to?: \BackedEnum|string|list<array{ // Default: []
228228+ * place?: string|Param,
229229+ * weight?: int|Param, // Default: 1
230230+ * }>,
231231+ * weight?: int|Param, // Default: 1
232232+ * metadata?: array<string, mixed>,
233233+ * }>,
234234+ * metadata?: array<string, mixed>,
235235+ * }>,
236236+ * },
237237+ * router?: bool|array{ // Router configuration
238238+ * enabled?: bool|Param, // Default: false
239239+ * resource?: scalar|Param|null,
240240+ * type?: scalar|Param|null,
241241+ * default_uri?: scalar|Param|null, // The default URI used to generate URLs in a non-HTTP context. // Default: null
242242+ * http_port?: scalar|Param|null, // Default: 80
243243+ * https_port?: scalar|Param|null, // Default: 443
244244+ * strict_requirements?: scalar|Param|null, // set to true to throw an exception when a parameter does not match the requirements set to false to disable exceptions when a parameter does not match the requirements (and return null instead) set to null to disable parameter checks against requirements 'true' is the preferred configuration in development mode, while 'false' or 'null' might be preferred in production // Default: true
245245+ * utf8?: bool|Param, // Default: true
246246+ * },
247247+ * session?: bool|array{ // Session configuration
248248+ * enabled?: bool|Param, // Default: false
249249+ * storage_factory_id?: scalar|Param|null, // Default: "session.storage.factory.native"
250250+ * handler_id?: scalar|Param|null, // Defaults to using the native session handler, or to the native *file* session handler if "save_path" is not null.
251251+ * name?: scalar|Param|null,
252252+ * cookie_lifetime?: scalar|Param|null,
253253+ * cookie_path?: scalar|Param|null,
254254+ * cookie_domain?: scalar|Param|null,
255255+ * cookie_secure?: true|false|"auto"|Param, // Default: "auto"
256256+ * cookie_httponly?: bool|Param, // Default: true
257257+ * cookie_samesite?: null|"lax"|"strict"|"none"|Param, // Default: "lax"
258258+ * use_cookies?: bool|Param,
259259+ * gc_divisor?: scalar|Param|null,
260260+ * gc_probability?: scalar|Param|null,
261261+ * gc_maxlifetime?: scalar|Param|null,
262262+ * save_path?: scalar|Param|null, // Defaults to "%kernel.cache_dir%/sessions" if the "handler_id" option is not null.
263263+ * metadata_update_threshold?: int|Param, // Seconds to wait between 2 session metadata updates. // Default: 0
264264+ * },
265265+ * request?: bool|array{ // Request configuration
266266+ * enabled?: bool|Param, // Default: false
267267+ * formats?: array<string, string|list<scalar|Param|null>>,
268268+ * },
269269+ * assets?: bool|array{ // Assets configuration
270270+ * enabled?: bool|Param, // Default: false
271271+ * strict_mode?: bool|Param, // Throw an exception if an entry is missing from the manifest.json. // Default: false
272272+ * version_strategy?: scalar|Param|null, // Default: null
273273+ * version?: scalar|Param|null, // Default: null
274274+ * version_format?: scalar|Param|null, // Default: "%%s?%%s"
275275+ * json_manifest_path?: scalar|Param|null, // Default: null
276276+ * base_path?: scalar|Param|null, // Default: ""
277277+ * base_urls?: string|list<scalar|Param|null>,
278278+ * packages?: array<string, array{ // Default: []
279279+ * strict_mode?: bool|Param, // Throw an exception if an entry is missing from the manifest.json. // Default: false
280280+ * version_strategy?: scalar|Param|null, // Default: null
281281+ * version?: scalar|Param|null,
282282+ * version_format?: scalar|Param|null, // Default: null
283283+ * json_manifest_path?: scalar|Param|null, // Default: null
284284+ * base_path?: scalar|Param|null, // Default: ""
285285+ * base_urls?: string|list<scalar|Param|null>,
286286+ * }>,
287287+ * },
288288+ * asset_mapper?: bool|array{ // Asset Mapper configuration
289289+ * enabled?: bool|Param, // Default: false
290290+ * paths?: string|array<string, scalar|Param|null>,
291291+ * excluded_patterns?: list<scalar|Param|null>,
292292+ * exclude_dotfiles?: bool|Param, // If true, any files starting with "." will be excluded from the asset mapper. // Default: true
293293+ * server?: bool|Param, // If true, a "dev server" will return the assets from the public directory (true in "debug" mode only by default). // Default: true
294294+ * public_prefix?: scalar|Param|null, // The public path where the assets will be written to (and served from when "server" is true). // Default: "/assets/"
295295+ * missing_import_mode?: "strict"|"warn"|"ignore"|Param, // Behavior if an asset cannot be found when imported from JavaScript or CSS files - e.g. "import './non-existent.js'". "strict" means an exception is thrown, "warn" means a warning is logged, "ignore" means the import is left as-is. // Default: "warn"
296296+ * extensions?: array<string, scalar|Param|null>,
297297+ * importmap_path?: scalar|Param|null, // The path of the importmap.php file. // Default: "%kernel.project_dir%/importmap.php"
298298+ * importmap_polyfill?: scalar|Param|null, // The importmap name that will be used to load the polyfill. Set to false to disable. // Default: "es-module-shims"
299299+ * importmap_script_attributes?: array<string, scalar|Param|null>,
300300+ * vendor_dir?: scalar|Param|null, // The directory to store JavaScript vendors. // Default: "%kernel.project_dir%/assets/vendor"
301301+ * precompress?: bool|array{ // Precompress assets with Brotli, Zstandard and gzip.
302302+ * enabled?: bool|Param, // Default: false
303303+ * formats?: list<scalar|Param|null>,
304304+ * extensions?: list<scalar|Param|null>,
305305+ * },
306306+ * },
307307+ * translator?: bool|array{ // Translator configuration
308308+ * enabled?: bool|Param, // Default: false
309309+ * fallbacks?: string|list<scalar|Param|null>,
310310+ * logging?: bool|Param, // Default: false
311311+ * formatter?: scalar|Param|null, // Default: "translator.formatter.default"
312312+ * cache_dir?: scalar|Param|null, // Default: "%kernel.cache_dir%/translations"
313313+ * default_path?: scalar|Param|null, // The default path used to load translations. // Default: "%kernel.project_dir%/translations"
314314+ * paths?: list<scalar|Param|null>,
315315+ * pseudo_localization?: bool|array{
316316+ * enabled?: bool|Param, // Default: false
317317+ * accents?: bool|Param, // Default: true
318318+ * expansion_factor?: float|Param, // Default: 1.0
319319+ * brackets?: bool|Param, // Default: true
320320+ * parse_html?: bool|Param, // Default: false
321321+ * localizable_html_attributes?: list<scalar|Param|null>,
322322+ * },
323323+ * providers?: array<string, array{ // Default: []
324324+ * dsn?: scalar|Param|null,
325325+ * domains?: list<scalar|Param|null>,
326326+ * locales?: list<scalar|Param|null>,
327327+ * }>,
328328+ * globals?: array<string, string|array{ // Default: []
329329+ * value?: mixed,
330330+ * message?: string|Param,
331331+ * parameters?: array<string, scalar|Param|null>,
332332+ * domain?: string|Param,
333333+ * }>,
334334+ * },
335335+ * validation?: bool|array{ // Validation configuration
336336+ * enabled?: bool|Param, // Default: false
337337+ * enable_attributes?: bool|Param, // Default: true
338338+ * static_method?: string|list<scalar|Param|null>,
339339+ * translation_domain?: scalar|Param|null, // Default: "validators"
340340+ * email_validation_mode?: "html5"|"html5-allow-no-tld"|"strict"|Param, // Default: "html5"
341341+ * mapping?: array{
342342+ * paths?: list<scalar|Param|null>,
343343+ * },
344344+ * not_compromised_password?: bool|array{
345345+ * enabled?: bool|Param, // When disabled, compromised passwords will be accepted as valid. // Default: true
346346+ * endpoint?: scalar|Param|null, // API endpoint for the NotCompromisedPassword Validator. // Default: null
347347+ * },
348348+ * disable_translation?: bool|Param, // Default: false
349349+ * property_metadata_existence_check?: bool|Param, // When enabled, validateProperty() and validatePropertyValue() throw an exception if no metadata is found for the given property. // Default: false
350350+ * auto_mapping?: array<string, array{ // Default: []
351351+ * services?: list<scalar|Param|null>,
352352+ * }>,
353353+ * },
354354+ * serializer?: bool|array{ // Serializer configuration
355355+ * enabled?: bool|Param, // Default: false
356356+ * enable_attributes?: bool|Param, // Default: true
357357+ * name_converter?: scalar|Param|null,
358358+ * circular_reference_handler?: scalar|Param|null,
359359+ * max_depth_handler?: scalar|Param|null,
360360+ * mapping?: array{
361361+ * paths?: list<scalar|Param|null>,
362362+ * },
363363+ * default_context?: array<string, mixed>,
364364+ * named_serializers?: array<string, array{ // Default: []
365365+ * name_converter?: scalar|Param|null,
366366+ * default_context?: array<string, mixed>,
367367+ * include_built_in_normalizers?: bool|Param, // Whether to include the built-in normalizers // Default: true
368368+ * include_built_in_encoders?: bool|Param, // Whether to include the built-in encoders // Default: true
369369+ * }>,
370370+ * },
371371+ * property_access?: bool|array{ // Property access configuration
372372+ * enabled?: bool|Param, // Default: false
373373+ * magic_call?: bool|Param, // Default: false
374374+ * magic_get?: bool|Param, // Default: true
375375+ * magic_set?: bool|Param, // Default: true
376376+ * throw_exception_on_invalid_index?: bool|Param, // Default: false
377377+ * throw_exception_on_invalid_property_path?: bool|Param, // Default: true
378378+ * },
379379+ * type_info?: bool|array{ // Type info configuration
380380+ * enabled?: bool|Param, // Default: false
381381+ * aliases?: array<string, scalar|Param|null>,
382382+ * },
383383+ * property_info?: bool|array{ // Property info configuration
384384+ * enabled?: bool|Param, // Default: false
385385+ * with_constructor_extractor?: bool|Param, // Registers the constructor extractor. // Default: true
386386+ * },
387387+ * cache?: array{ // Cache configuration
388388+ * prefix_seed?: scalar|Param|null, // Used to namespace cache keys when using several apps with the same shared backend. // Default: "_%kernel.project_dir%.%kernel.container_class%"
389389+ * app?: scalar|Param|null, // App related cache pools configuration. // Default: "cache.adapter.filesystem"
390390+ * system?: scalar|Param|null, // System related cache pools configuration. // Default: "cache.adapter.system"
391391+ * directory?: scalar|Param|null, // Default: "%kernel.share_dir%/pools/app"
392392+ * default_psr6_provider?: scalar|Param|null,
393393+ * default_redis_provider?: scalar|Param|null, // Default: "redis://localhost"
394394+ * default_valkey_provider?: scalar|Param|null, // Default: "valkey://localhost"
395395+ * default_memcached_provider?: scalar|Param|null, // Default: "memcached://localhost"
396396+ * default_doctrine_dbal_provider?: scalar|Param|null, // Default: "database_connection"
397397+ * default_pdo_provider?: scalar|Param|null, // Default: null
398398+ * pools?: array<string, array{ // Default: []
399399+ * adapters?: string|list<scalar|Param|null>,
400400+ * tags?: scalar|Param|null, // Default: null
401401+ * public?: bool|Param, // Default: false
402402+ * default_lifetime?: scalar|Param|null, // Default lifetime of the pool.
403403+ * provider?: scalar|Param|null, // Overwrite the setting from the default provider for this adapter.
404404+ * early_expiration_message_bus?: scalar|Param|null,
405405+ * clearer?: scalar|Param|null,
406406+ * marshaller?: scalar|Param|null, // The marshaller service to use for this pool.
407407+ * }>,
408408+ * },
409409+ * php_errors?: array{ // PHP errors handling configuration
410410+ * log?: mixed, // Use the application logger instead of the PHP logger for logging PHP errors. // Default: true
411411+ * throw?: bool|Param, // Throw PHP errors as \ErrorException instances. // Default: true
412412+ * },
413413+ * exceptions?: array<string, array{ // Default: []
414414+ * log_level?: scalar|Param|null, // The level of log message. Null to let Symfony decide. // Default: null
415415+ * status_code?: scalar|Param|null, // The status code of the response. Null or 0 to let Symfony decide. // Default: null
416416+ * log_channel?: scalar|Param|null, // The channel of log message. Null to let Symfony decide. // Default: null
417417+ * }>,
418418+ * web_link?: bool|array{ // Web links configuration
419419+ * enabled?: bool|Param, // Default: false
420420+ * },
421421+ * lock?: bool|string|array{ // Lock configuration
422422+ * enabled?: bool|Param, // Default: false
423423+ * resources?: string|array<string, string|list<scalar|Param|null>>,
424424+ * },
425425+ * semaphore?: bool|string|array{ // Semaphore configuration
426426+ * enabled?: bool|Param, // Default: false
427427+ * resources?: string|array<string, scalar|Param|null>,
428428+ * },
429429+ * messenger?: bool|array{ // Messenger configuration
430430+ * enabled?: bool|Param, // Default: false
431431+ * routing?: array<string, string|list<scalar|Param|null>>,
432432+ * serializer?: array{
433433+ * default_serializer?: scalar|Param|null, // Service id to use as the default serializer for the transports. // Default: "messenger.transport.native_php_serializer"
434434+ * symfony_serializer?: array{
435435+ * format?: scalar|Param|null, // Serialization format for the messenger.transport.symfony_serializer service (which is not the serializer used by default). // Default: "json"
436436+ * context?: array<string, mixed>,
437437+ * },
438438+ * },
439439+ * transports?: array<string, string|array{ // Default: []
440440+ * dsn?: scalar|Param|null,
441441+ * serializer?: scalar|Param|null, // Service id of a custom serializer to use. // Default: null
442442+ * options?: array<string, mixed>,
443443+ * failure_transport?: scalar|Param|null, // Transport name to send failed messages to (after all retries have failed). // Default: null
444444+ * retry_strategy?: string|array{
445445+ * service?: scalar|Param|null, // Service id to override the retry strategy entirely. // Default: null
446446+ * max_retries?: int|Param, // Default: 3
447447+ * delay?: int|Param, // Time in ms to delay (or the initial value when multiplier is used). // Default: 1000
448448+ * multiplier?: float|Param, // If greater than 1, delay will grow exponentially for each retry: this delay = (delay * (multiple ^ retries)). // Default: 2
449449+ * max_delay?: int|Param, // Max time in ms that a retry should ever be delayed (0 = infinite). // Default: 0
450450+ * jitter?: float|Param, // Randomness to apply to the delay (between 0 and 1). // Default: 0.1
451451+ * },
452452+ * rate_limiter?: scalar|Param|null, // Rate limiter name to use when processing messages. // Default: null
453453+ * }>,
454454+ * failure_transport?: scalar|Param|null, // Transport name to send failed messages to (after all retries have failed). // Default: null
455455+ * stop_worker_on_signals?: int|string|list<scalar|Param|null>,
456456+ * default_bus?: scalar|Param|null, // Default: null
457457+ * buses?: array<string, array{ // Default: {"messenger.bus.default":{"default_middleware":{"enabled":true,"allow_no_handlers":false,"allow_no_senders":true},"middleware":[]}}
458458+ * default_middleware?: bool|string|array{
459459+ * enabled?: bool|Param, // Default: true
460460+ * allow_no_handlers?: bool|Param, // Default: false
461461+ * allow_no_senders?: bool|Param, // Default: true
462462+ * },
463463+ * middleware?: string|list<string|array{ // Default: []
464464+ * id?: scalar|Param|null,
465465+ * arguments?: list<mixed>,
466466+ * }>,
467467+ * }>,
468468+ * },
469469+ * scheduler?: bool|array{ // Scheduler configuration
470470+ * enabled?: bool|Param, // Default: false
471471+ * },
472472+ * disallow_search_engine_index?: bool|Param, // Enabled by default when debug is enabled. // Default: true
473473+ * http_client?: bool|array{ // HTTP Client configuration
474474+ * enabled?: bool|Param, // Default: false
475475+ * max_host_connections?: int|Param, // The maximum number of connections to a single host.
476476+ * default_options?: array{
477477+ * headers?: array<string, mixed>,
478478+ * vars?: array<string, mixed>,
479479+ * max_redirects?: int|Param, // The maximum number of redirects to follow.
480480+ * http_version?: scalar|Param|null, // The default HTTP version, typically 1.1 or 2.0, leave to null for the best version.
481481+ * resolve?: array<string, scalar|Param|null>,
482482+ * proxy?: scalar|Param|null, // The URL of the proxy to pass requests through or null for automatic detection.
483483+ * no_proxy?: scalar|Param|null, // A comma separated list of hosts that do not require a proxy to be reached.
484484+ * timeout?: float|Param, // The idle timeout, defaults to the "default_socket_timeout" ini parameter.
485485+ * max_duration?: float|Param, // The maximum execution time for the request+response as a whole.
486486+ * bindto?: scalar|Param|null, // A network interface name, IP address, a host name or a UNIX socket to bind to.
487487+ * verify_peer?: bool|Param, // Indicates if the peer should be verified in a TLS context.
488488+ * verify_host?: bool|Param, // Indicates if the host should exist as a certificate common name.
489489+ * cafile?: scalar|Param|null, // A certificate authority file.
490490+ * capath?: scalar|Param|null, // A directory that contains multiple certificate authority files.
491491+ * local_cert?: scalar|Param|null, // A PEM formatted certificate file.
492492+ * local_pk?: scalar|Param|null, // A private key file.
493493+ * passphrase?: scalar|Param|null, // The passphrase used to encrypt the "local_pk" file.
494494+ * ciphers?: scalar|Param|null, // A list of TLS ciphers separated by colons, commas or spaces (e.g. "RC3-SHA:TLS13-AES-128-GCM-SHA256"...)
495495+ * peer_fingerprint?: array{ // Associative array: hashing algorithm => hash(es).
496496+ * sha1?: mixed,
497497+ * pin-sha256?: mixed,
498498+ * md5?: mixed,
499499+ * },
500500+ * crypto_method?: scalar|Param|null, // The minimum version of TLS to accept; must be one of STREAM_CRYPTO_METHOD_TLSv*_CLIENT constants.
501501+ * extra?: array<string, mixed>,
502502+ * rate_limiter?: scalar|Param|null, // Rate limiter name to use for throttling requests. // Default: null
503503+ * caching?: bool|array{ // Caching configuration.
504504+ * enabled?: bool|Param, // Default: false
505505+ * cache_pool?: string|Param, // The taggable cache pool to use for storing the responses. // Default: "cache.http_client"
506506+ * shared?: bool|Param, // Indicates whether the cache is shared (public) or private. // Default: true
507507+ * max_ttl?: int|Param, // The maximum TTL (in seconds) allowed for cached responses. // Default: 86400
508508+ * },
509509+ * retry_failed?: bool|array{
510510+ * enabled?: bool|Param, // Default: false
511511+ * retry_strategy?: scalar|Param|null, // service id to override the retry strategy. // Default: null
512512+ * http_codes?: int|string|array<string, array{ // Default: []
513513+ * code?: int|Param,
514514+ * methods?: string|list<string|Param>,
515515+ * }>,
516516+ * max_retries?: int|Param, // Default: 3
517517+ * delay?: int|Param, // Time in ms to delay (or the initial value when multiplier is used). // Default: 1000
518518+ * multiplier?: float|Param, // If greater than 1, delay will grow exponentially for each retry: delay * (multiple ^ retries). // Default: 2
519519+ * max_delay?: int|Param, // Max time in ms that a retry should ever be delayed (0 = infinite). // Default: 0
520520+ * jitter?: float|Param, // Randomness in percent (between 0 and 1) to apply to the delay. // Default: 0.1
521521+ * },
522522+ * },
523523+ * mock_response_factory?: scalar|Param|null, // `true` to always return empty 200 responses, or the id of the service to use to generate mock responses - which should be either an invokable or an iterable.
524524+ * scoped_clients?: array<string, string|array{ // Default: []
525525+ * scope?: scalar|Param|null, // The regular expression that the request URL must match before adding the other options. When none is provided, the base URI is used instead.
526526+ * base_uri?: scalar|Param|null, // The URI to resolve relative URLs, following rules in RFC 3985, section 2.
527527+ * auth_basic?: scalar|Param|null, // An HTTP Basic authentication "username:password".
528528+ * auth_bearer?: scalar|Param|null, // A token enabling HTTP Bearer authorization.
529529+ * auth_ntlm?: scalar|Param|null, // A "username:password" pair to use Microsoft NTLM authentication (requires the cURL extension).
530530+ * query?: array<string, scalar|Param|null>,
531531+ * headers?: array<string, mixed>,
532532+ * max_redirects?: int|Param, // The maximum number of redirects to follow.
533533+ * http_version?: scalar|Param|null, // The default HTTP version, typically 1.1 or 2.0, leave to null for the best version.
534534+ * resolve?: array<string, scalar|Param|null>,
535535+ * proxy?: scalar|Param|null, // The URL of the proxy to pass requests through or null for automatic detection.
536536+ * no_proxy?: scalar|Param|null, // A comma separated list of hosts that do not require a proxy to be reached.
537537+ * timeout?: float|Param, // The idle timeout, defaults to the "default_socket_timeout" ini parameter.
538538+ * max_duration?: float|Param, // The maximum execution time for the request+response as a whole.
539539+ * bindto?: scalar|Param|null, // A network interface name, IP address, a host name or a UNIX socket to bind to.
540540+ * verify_peer?: bool|Param, // Indicates if the peer should be verified in a TLS context.
541541+ * verify_host?: bool|Param, // Indicates if the host should exist as a certificate common name.
542542+ * cafile?: scalar|Param|null, // A certificate authority file.
543543+ * capath?: scalar|Param|null, // A directory that contains multiple certificate authority files.
544544+ * local_cert?: scalar|Param|null, // A PEM formatted certificate file.
545545+ * local_pk?: scalar|Param|null, // A private key file.
546546+ * passphrase?: scalar|Param|null, // The passphrase used to encrypt the "local_pk" file.
547547+ * ciphers?: scalar|Param|null, // A list of TLS ciphers separated by colons, commas or spaces (e.g. "RC3-SHA:TLS13-AES-128-GCM-SHA256"...).
548548+ * peer_fingerprint?: array{ // Associative array: hashing algorithm => hash(es).
549549+ * sha1?: mixed,
550550+ * pin-sha256?: mixed,
551551+ * md5?: mixed,
552552+ * },
553553+ * crypto_method?: scalar|Param|null, // The minimum version of TLS to accept; must be one of STREAM_CRYPTO_METHOD_TLSv*_CLIENT constants.
554554+ * mock_response_factory?: scalar|Param|null, // `true` to always return empty 200 responses, `false` to disable mocking, or the id of the service to use to generate mock responses (invokable or iterable).
555555+ * extra?: array<string, mixed>,
556556+ * rate_limiter?: scalar|Param|null, // Rate limiter name to use for throttling requests. // Default: null
557557+ * caching?: bool|array{ // Caching configuration.
558558+ * enabled?: bool|Param, // Default: false
559559+ * cache_pool?: string|Param, // The taggable cache pool to use for storing the responses. // Default: "cache.http_client"
560560+ * shared?: bool|Param, // Indicates whether the cache is shared (public) or private. // Default: true
561561+ * max_ttl?: int|Param, // The maximum TTL (in seconds) allowed for cached responses. // Default: 86400
562562+ * },
563563+ * retry_failed?: bool|array{
564564+ * enabled?: bool|Param, // Default: false
565565+ * retry_strategy?: scalar|Param|null, // service id to override the retry strategy. // Default: null
566566+ * http_codes?: int|string|array<string, array{ // Default: []
567567+ * code?: int|Param,
568568+ * methods?: string|list<string|Param>,
569569+ * }>,
570570+ * max_retries?: int|Param, // Default: 3
571571+ * delay?: int|Param, // Time in ms to delay (or the initial value when multiplier is used). // Default: 1000
572572+ * multiplier?: float|Param, // If greater than 1, delay will grow exponentially for each retry: delay * (multiple ^ retries). // Default: 2
573573+ * max_delay?: int|Param, // Max time in ms that a retry should ever be delayed (0 = infinite). // Default: 0
574574+ * jitter?: float|Param, // Randomness in percent (between 0 and 1) to apply to the delay. // Default: 0.1
575575+ * },
576576+ * }>,
577577+ * },
578578+ * mailer?: bool|array{ // Mailer configuration
579579+ * enabled?: bool|Param, // Default: false
580580+ * message_bus?: scalar|Param|null, // The message bus to use. Defaults to the default bus if the Messenger component is installed. // Default: null
581581+ * dsn?: scalar|Param|null, // Default: null
582582+ * transports?: array<string, scalar|Param|null>,
583583+ * envelope?: array{ // Mailer Envelope configuration
584584+ * sender?: scalar|Param|null,
585585+ * recipients?: string|list<scalar|Param|null>,
586586+ * allowed_recipients?: string|list<scalar|Param|null>,
587587+ * },
588588+ * headers?: array<string, string|array{ // Default: []
589589+ * value?: mixed,
590590+ * }>,
591591+ * dkim_signer?: bool|array{ // DKIM signer configuration
592592+ * enabled?: bool|Param, // Default: false
593593+ * key?: scalar|Param|null, // Key content, or path to key (in PEM format with the `file://` prefix) // Default: ""
594594+ * domain?: scalar|Param|null, // Default: ""
595595+ * select?: scalar|Param|null, // Default: ""
596596+ * passphrase?: scalar|Param|null, // The private key passphrase // Default: ""
597597+ * options?: array<string, mixed>,
598598+ * },
599599+ * smime_signer?: bool|array{ // S/MIME signer configuration
600600+ * enabled?: bool|Param, // Default: false
601601+ * key?: scalar|Param|null, // Path to key (in PEM format) // Default: ""
602602+ * certificate?: scalar|Param|null, // Path to certificate (in PEM format without the `file://` prefix) // Default: ""
603603+ * passphrase?: scalar|Param|null, // The private key passphrase // Default: null
604604+ * extra_certificates?: scalar|Param|null, // Default: null
605605+ * sign_options?: int|Param, // Default: null
606606+ * },
607607+ * smime_encrypter?: bool|array{ // S/MIME encrypter configuration
608608+ * enabled?: bool|Param, // Default: false
609609+ * repository?: scalar|Param|null, // S/MIME certificate repository service. This service shall implement the `Symfony\Component\Mailer\EventListener\SmimeCertificateRepositoryInterface`. // Default: ""
610610+ * cipher?: int|Param, // A set of algorithms used to encrypt the message // Default: null
611611+ * },
612612+ * },
613613+ * secrets?: bool|array{
614614+ * enabled?: bool|Param, // Default: true
615615+ * vault_directory?: scalar|Param|null, // Default: "%kernel.project_dir%/config/secrets/%kernel.runtime_environment%"
616616+ * local_dotenv_file?: scalar|Param|null, // Default: "%kernel.project_dir%/.env.%kernel.environment%.local"
617617+ * decryption_env_var?: scalar|Param|null, // Default: "base64:default::SYMFONY_DECRYPTION_SECRET"
618618+ * },
619619+ * notifier?: bool|array{ // Notifier configuration
620620+ * enabled?: bool|Param, // Default: false
621621+ * message_bus?: scalar|Param|null, // The message bus to use. Defaults to the default bus if the Messenger component is installed. // Default: null
622622+ * chatter_transports?: array<string, scalar|Param|null>,
623623+ * texter_transports?: array<string, scalar|Param|null>,
624624+ * notification_on_failed_messages?: bool|Param, // Default: false
625625+ * channel_policy?: array<string, string|list<scalar|Param|null>>,
626626+ * admin_recipients?: list<array{ // Default: []
627627+ * email?: scalar|Param|null,
628628+ * phone?: scalar|Param|null, // Default: ""
629629+ * }>,
630630+ * },
631631+ * rate_limiter?: bool|array{ // Rate limiter configuration
632632+ * enabled?: bool|Param, // Default: false
633633+ * limiters?: array<string, array{ // Default: []
634634+ * lock_factory?: scalar|Param|null, // The service ID of the lock factory used by this limiter (or null to disable locking). // Default: "auto"
635635+ * cache_pool?: scalar|Param|null, // The cache pool to use for storing the current limiter state. // Default: "cache.rate_limiter"
636636+ * storage_service?: scalar|Param|null, // The service ID of a custom storage implementation, this precedes any configured "cache_pool". // Default: null
637637+ * policy?: "fixed_window"|"token_bucket"|"sliding_window"|"compound"|"no_limit"|Param, // The algorithm to be used by this limiter.
638638+ * limiters?: string|list<scalar|Param|null>,
639639+ * limit?: int|Param, // The maximum allowed hits in a fixed interval or burst.
640640+ * interval?: scalar|Param|null, // Configures the fixed interval if "policy" is set to "fixed_window" or "sliding_window". The value must be a number followed by "second", "minute", "hour", "day", "week" or "month" (or their plural equivalent).
641641+ * rate?: array{ // Configures the fill rate if "policy" is set to "token_bucket".
642642+ * interval?: scalar|Param|null, // Configures the rate interval. The value must be a number followed by "second", "minute", "hour", "day", "week" or "month" (or their plural equivalent).
643643+ * amount?: int|Param, // Amount of tokens to add each interval. // Default: 1
644644+ * },
645645+ * anchor_at?: scalar|Param|null, // Aligns the "fixed_window" policy to a calendar (e.g. "2024-01-05 00:00:00 UTC" combined with `interval: 1 month` resets the counter on the 5th of each month). UTC if not specified. // Default: null
646646+ * }>,
647647+ * },
648648+ * uid?: bool|array{ // Uid configuration
649649+ * enabled?: bool|Param, // Default: false
650650+ * default_uuid_version?: 7|6|4|1|Param, // Default: 7
651651+ * name_based_uuid_version?: 5|3|Param, // Default: 5
652652+ * name_based_uuid_namespace?: scalar|Param|null,
653653+ * time_based_uuid_version?: 7|6|1|Param, // Default: 7
654654+ * time_based_uuid_node?: scalar|Param|null,
655655+ * uuid47_secret?: scalar|Param|null, // A high-entropy secret used by the "uuid47_transformer" service. Defaults to "kernel.secret". // Default: null
656656+ * },
657657+ * html_sanitizer?: bool|array{ // HtmlSanitizer configuration
658658+ * enabled?: bool|Param, // Default: false
659659+ * sanitizers?: array<string, array{ // Default: []
660660+ * default_action?: "drop"|"block"|"allow"|Param, // Defines how the sanitizer must behave by default.
661661+ * allow_safe_elements?: bool|Param, // Allows "safe" elements and attributes. // Default: false
662662+ * allow_static_elements?: bool|Param, // Allows all static elements and attributes from the W3C Sanitizer API standard. // Default: false
663663+ * allow_elements?: array<string, mixed>,
664664+ * block_elements?: string|list<string|Param>,
665665+ * drop_elements?: string|list<string|Param>,
666666+ * allow_attributes?: array<string, mixed>,
667667+ * drop_attributes?: array<string, mixed>,
668668+ * force_attributes?: array<string, array<string, string|Param>>,
669669+ * force_https_urls?: bool|Param, // Transforms URLs using the HTTP scheme to use the HTTPS scheme instead. // Default: false
670670+ * allowed_link_schemes?: string|list<string|Param>,
671671+ * allowed_link_hosts?: null|string|list<string|Param>,
672672+ * allow_relative_links?: bool|Param, // Allows relative URLs to be used in links href attributes. // Default: false
673673+ * allowed_media_schemes?: string|list<string|Param>,
674674+ * allowed_media_hosts?: null|string|list<string|Param>,
675675+ * allow_relative_medias?: bool|Param, // Allows relative URLs to be used in media source attributes (img, audio, video, ...). // Default: false
676676+ * with_attribute_sanitizers?: string|list<string|Param>,
677677+ * without_attribute_sanitizers?: string|list<string|Param>,
678678+ * max_input_length?: int|Param, // The maximum length allowed for the sanitized input. // Default: 0
679679+ * }>,
680680+ * },
681681+ * webhook?: bool|array{ // Webhook configuration
682682+ * enabled?: bool|Param, // Default: false
683683+ * message_bus?: scalar|Param|null, // The message bus to use. // Default: "messenger.default_bus"
684684+ * event_header_name?: scalar|Param|null, // Default: "Webhook-Event"
685685+ * id_header_name?: scalar|Param|null, // Default: "Webhook-Id"
686686+ * signature_header_name?: scalar|Param|null, // Default: "Webhook-Signature"
687687+ * signing_algorithm?: scalar|Param|null, // Default: "sha256"
688688+ * routing?: array<string, array{ // Default: []
689689+ * service?: scalar|Param|null,
690690+ * secret?: scalar|Param|null, // Default: ""
691691+ * }>,
692692+ * },
693693+ * remote-event?: bool|array{ // RemoteEvent configuration
694694+ * enabled?: bool|Param, // Default: false
695695+ * },
696696+ * json_streamer?: bool|array{ // JSON streamer configuration
697697+ * enabled?: bool|Param, // Default: false
698698+ * default_options?: array{
699699+ * include_null_properties?: bool|Param, // Encode the properties with null value // Default: false
700700+ * ...<string, mixed>
701701+ * },
702702+ * },
703703+ * }
704704+ * @psalm-type ConfigType = array{
705705+ * imports?: ImportsConfig,
706706+ * parameters?: ParametersConfig,
707707+ * services?: ServicesConfig,
708708+ * framework?: FrameworkConfig,
709709+ * "when@dev"?: array{
710710+ * imports?: ImportsConfig,
711711+ * parameters?: ParametersConfig,
712712+ * services?: ServicesConfig,
713713+ * framework?: FrameworkConfig,
714714+ * },
715715+ * "when@prod"?: array{
716716+ * imports?: ImportsConfig,
717717+ * parameters?: ParametersConfig,
718718+ * services?: ServicesConfig,
719719+ * framework?: FrameworkConfig,
720720+ * },
721721+ * "when@test"?: array{
722722+ * imports?: ImportsConfig,
723723+ * parameters?: ParametersConfig,
724724+ * services?: ServicesConfig,
725725+ * framework?: FrameworkConfig,
726726+ * },
727727+ * ...<string, ExtensionType|array{ // extra keys must follow the when@%env% pattern or match an extension alias
728728+ * imports?: ImportsConfig,
729729+ * parameters?: ParametersConfig,
730730+ * services?: ServicesConfig,
731731+ * ...<string, ExtensionType>,
732732+ * }>
733733+ * }
734734+ */
735735+final class App
736736+{
737737+ /**
738738+ * @param ConfigType $config
739739+ *
740740+ * @psalm-return ConfigType
741741+ */
742742+ public static function config(array $config): array
743743+ {
744744+ /** @var ConfigType $config */
745745+ $config = AppReference::config($config);
746746+747747+ return $config;
748748+ }
749749+}
750750+751751+namespace Symfony\Component\Routing\Loader\Configurator;
752752+753753+/**
754754+ * This class provides array-shapes for configuring the routes of an application.
755755+ *
756756+ * Example:
757757+ *
758758+ * ```php
759759+ * // config/routes.php
760760+ * namespace Symfony\Component\Routing\Loader\Configurator;
761761+ *
762762+ * return Routes::config([
763763+ * 'controllers' => [
764764+ * 'resource' => 'routing.controllers',
765765+ * ],
766766+ * ]);
767767+ * ```
768768+ *
769769+ * @psalm-type RouteConfig = array{
770770+ * path: string|array<string,string>,
771771+ * controller?: string,
772772+ * methods?: string|list<string>,
773773+ * requirements?: array<string,string>,
774774+ * defaults?: array<string,mixed>,
775775+ * options?: array<string,mixed>,
776776+ * host?: string|array<string,string>,
777777+ * schemes?: string|list<string>,
778778+ * condition?: string,
779779+ * locale?: string,
780780+ * format?: string,
781781+ * utf8?: bool,
782782+ * stateless?: bool,
783783+ * }
784784+ * @psalm-type ImportConfig = array{
785785+ * resource: string,
786786+ * type?: string,
787787+ * exclude?: string|list<string>,
788788+ * prefix?: string|array<string,string>,
789789+ * name_prefix?: string,
790790+ * trailing_slash_on_root?: bool,
791791+ * controller?: string,
792792+ * methods?: string|list<string>,
793793+ * requirements?: array<string,string>,
794794+ * defaults?: array<string,mixed>,
795795+ * options?: array<string,mixed>,
796796+ * host?: string|array<string,string>,
797797+ * schemes?: string|list<string>,
798798+ * condition?: string,
799799+ * locale?: string,
800800+ * format?: string,
801801+ * utf8?: bool,
802802+ * stateless?: bool,
803803+ * }
804804+ * @psalm-type AliasConfig = array{
805805+ * alias: string,
806806+ * deprecated?: array{package:string, version:string, message?:string},
807807+ * }
808808+ * @psalm-type RoutesConfig = array{
809809+ * "when@dev"?: array<string, RouteConfig|ImportConfig|AliasConfig>,
810810+ * "when@prod"?: array<string, RouteConfig|ImportConfig|AliasConfig>,
811811+ * "when@test"?: array<string, RouteConfig|ImportConfig|AliasConfig>,
812812+ * ...<string, RouteConfig|ImportConfig|AliasConfig>
813813+ * }
814814+ */
815815+final class Routes
816816+{
817817+ /**
818818+ * @param RoutesConfig $config
819819+ *
820820+ * @psalm-return RoutesConfig
821821+ */
822822+ public static function config(array $config): array
823823+ {
824824+ return $config;
825825+ }
826826+}
+11
application/config/routes.yaml
···11+# yaml-language-server: $schema=../vendor/symfony/routing/Loader/schema/routing.schema.json
22+33+# This file is the entry point to configure the routes of your app.
44+# Methods with the #[Route] attribute are automatically imported.
55+# See also https://symfony.com/doc/current/routing.html
66+77+# To list all registered routes, run the following command:
88+# bin/console debug:router
99+1010+controllers:
1111+ resource: routing.controllers
···11+# yaml-language-server: $schema=../vendor/symfony/dependency-injection/Loader/schema/services.schema.json
22+33+# This file is the entry point to configure your own services.
44+# Files in the packages/ subdirectory configure your dependencies.
55+# See also https://symfony.com/doc/current/service_container/import.html
66+77+# Put parameters here that don't need to change on each machine where the app is deployed
88+# https://symfony.com/doc/current/best_practices.html#use-parameters-for-application-configuration
99+parameters:
1010+1111+services:
1212+ # default configuration for services in *this* file
1313+ _defaults:
1414+ autowire: true # Automatically injects dependencies in your services.
1515+ autoconfigure: true # Automatically registers your services as commands, event subscribers, etc.
1616+1717+ # makes classes in src/ available to be used as services
1818+ # this creates a service per class whose id is the fully-qualified class name
1919+ App\:
2020+ resource: '../src/'
2121+2222+ # add more service definitions when explicit configuration is needed
2323+ # please note that last definitions always *replace* previous ones
···11+# Using AI coding agents
22+33+This project ships a [Dev Container](https://containers.dev/) ready for AI coding agents.
44+No agent is installed by default: you pick one, add it to `.devcontainer/devcontainer.json`,
55+and rebuild the container. The recommended agent is [OpenCode](https://opencode.ai), which is
66+open source and works with both local and remote models.
77+88+You can run an agent against a model on your own machine (no data leaves your computer) or
99+against a remote provider. An optional [network firewall](#optional-network-sandbox) restricts
1010+the container's outbound traffic when you let an agent run autonomously.
1111+1212+## Prerequisites
1313+1414+- [Docker Engine](https://docs.docker.com/engine/) (or any Docker-compatible runtime)
1515+- [A _Development Container_-compatible editor](https://containers.dev/supporting#editors) (Visual Studio Code, PhpStorm, Emacs...)
1616+1717+In Visual Studio Code, after editing `.devcontainer/devcontainer.json`, run **Dev Containers: Rebuild Container** from
1818+the Command Palette (`Ctrl+Shift+P` / `Cmd+Shift+P`) to apply the change.
1919+2020+## OpenCode (recommended)
2121+2222+There is no official Dev Container feature for OpenCode yet, so install the CLI from the
2323+`postCreateCommand` and add the Visual Studio Code extension. Edit `.devcontainer/devcontainer.json`:
2424+2525+```jsonc
2626+{
2727+ // Install the CLI on container creation (alongside the existing intelephense install)
2828+ "postCreateCommand": "npm install -g intelephense && curl -fsSL https://opencode.ai/install | bash",
2929+ "customizations": {
3030+ "vscode": {
3131+ "extensions": [
3232+ "sst-dev.opencode",
3333+ "bmewburn.vscode-intelephense-client",
3434+ "xdebug.php-debug",
3535+ ],
3636+ },
3737+ },
3838+}
3939+```
4040+4141+Rebuild the container, then run `opencode` in the integrated terminal or open the OpenCode panel.
4242+4343+### Using a local model
4444+4545+Run [Ollama](https://ollama.com) (FOSS) or [LM Studio](https://lmstudio.ai) (proprietary) on your host machine. The
4646+Dev Container reaches the host through `host.docker.internal`, which is already mapped in
4747+`compose.override.yaml`. No internet access is required, and this keeps working with the
4848+[firewall](#optional-network-sandbox) enabled (private networks and the host gateway are allowed).
4949+5050+Point OpenCode at the host endpoint, for example Ollama on its default port:
5151+5252+```console
5353+http://host.docker.internal:11434
5454+```
5555+5656+LM Studio listens on `http://host.docker.internal:1234` by default. See the
5757+[OpenCode providers documentation](https://opencode.ai/docs/providers/) for the exact
5858+configuration.
5959+6060+### Using a remote model
6161+6262+Set the API key for your provider (Anthropic, OpenAI, OpenRouter, etc.) as documented by
6363+OpenCode. With the [firewall](#optional-network-sandbox) enabled, add the provider's API domain
6464+to the allowlist.
6565+6666+## Claude Code
6767+6868+Install [Claude Code](https://claude.ai/claude-code) (proprietary) through a Dev Container feature and add its
6969+Visual Studio Code extension. Edit `.devcontainer/devcontainer.json`:
7070+7171+```jsonc
7272+{
7373+ "features": {
7474+ "ghcr.io/devcontainers/features/node:1": {},
7575+ "ghcr.io/devcontainers-extra/features/claude-code:2": {},
7676+ },
7777+ "customizations": {
7878+ "vscode": {
7979+ "extensions": [
8080+ "anthropic.claude-code",
8181+ "bmewburn.vscode-intelephense-client",
8282+ "xdebug.php-debug",
8383+ ],
8484+ },
8585+ },
8686+}
8787+```
8888+8989+Rebuild the container, then run `claude` in the integrated terminal or open the Claude Code panel.
9090+9191+Without the firewall, this is all you need. To let Claude Code run autonomously, enable the
9292+[network sandbox](#optional-network-sandbox) first and add `anthropic.com`, `sentry.io`, and
9393+`statsig.com` to the allowlist.
9494+9595+## Optional: network sandbox
9696+9797+Letting an agent edit files and run commands without confirmation (autonomous, or "YOLO", mode)
9898+is convenient but unguarded. Pair it with a firewall that restricts the container's outbound
9999+traffic to a short allowlist. Only run an agent autonomously with the firewall on.
100100+101101+### 1. Add the required tools to the dev image
102102+103103+Edit the `frankenphp_dev` stage in the `Dockerfile`:
104104+105105+```dockerfile
106106+# hadolint ignore=DL3008
107107+RUN <<-EOF
108108+ mv "$PHP_INI_DIR/php.ini-development" "$PHP_INI_DIR/php.ini"
109109+ apt-get update
110110+ apt-get install -y --no-install-recommends \
111111+ aggregate \
112112+ curl \
113113+ dnsmasq \
114114+ dnsutils \
115115+ iproute2 \
116116+ ipset \
117117+ iptables \
118118+ jq \
119119+ sudo
120120+ install-php-extensions xdebug
121121+ rm -rf /var/lib/apt/lists/*
122122+ useradd -m -s /bin/bash nonroot
123123+ # Allow nonroot to run only the firewall script as root, without a password
124124+ echo "nonroot ALL=(root) NOPASSWD: /app/.devcontainer/init-firewall.sh" > /etc/sudoers.d/init-firewall
125125+ chmod 0440 /etc/sudoers.d/init-firewall
126126+ git config --system --add safe.directory /app
127127+EOF
128128+```
129129+130130+### 2. Grant the firewall capability
131131+132132+The firewall needs `NET_ADMIN`. Add it to the `php` service in
133133+`.devcontainer/compose.devcontainer.yaml`:
134134+135135+```yaml
136136+services:
137137+ php:
138138+ cap_add:
139139+ - NET_ADMIN
140140+```
141141+142142+### 3. Add the firewall script
143143+144144+Save the following as `.devcontainer/init-firewall.sh`. It uses `iptables` and `ipset` to drop
145145+all outbound traffic except the allowlisted domains, and [dnsmasq](https://thekelleys.org.uk/dnsmasq/doc.html)
146146+to resolve those domains dynamically so CDN IP rotation is handled gracefully.
147147+148148+```bash
149149+#!/bin/bash
150150+# Locks down outbound network access to an allowlist using iptables + ipset.
151151+# dnsmasq intercepts DNS queries and adds the resolved IPs for allowed domains
152152+# to the ipset dynamically, so the ipset stays current despite CDN IP rotation.
153153+set -euo pipefail
154154+IFS=$'\n\t'
155155+156156+# 1. Extract Docker DNS info BEFORE any flushing
157157+DOCKER_DNS_RULES=$(iptables-save -t nat | grep "127\.0\.0\.11" || true)
158158+159159+# Flush existing rules and delete existing ipsets
160160+iptables -F
161161+iptables -X
162162+iptables -t nat -F
163163+iptables -t nat -X
164164+iptables -t mangle -F
165165+iptables -t mangle -X
166166+ipset destroy allowed-domains 2>/dev/null || true
167167+168168+# 2. Selectively restore ONLY internal Docker DNS resolution
169169+# These rules redirect queries to 127.0.0.11:53 to Docker's actual DNS port.
170170+# These are restored first; later, our DNS redirect rules are inserted with
171171+# `iptables -t nat -I OUTPUT ...`, so they precede these Docker rules. Only
172172+# dnsmasq's upstream queries (which are exempt from our redirect) pass through them.
173173+if [ -n "$DOCKER_DNS_RULES" ]; then
174174+ echo "Restoring Docker DNS rules..."
175175+ iptables -t nat -N DOCKER_OUTPUT 2>/dev/null || true
176176+ iptables -t nat -N DOCKER_POSTROUTING 2>/dev/null || true
177177+ echo "$DOCKER_DNS_RULES" | xargs -L 1 iptables -t nat
178178+else
179179+ echo "No Docker DNS rules to restore"
180180+fi
181181+182182+# Allow DNS (outbound only; return traffic is handled by ESTABLISHED,RELATED)
183183+# and localhost (covers dnsmasq ↔ Docker DNS on 127.0.0.11).
184184+iptables -A OUTPUT -p udp --dport 53 -j ACCEPT
185185+iptables -A OUTPUT -p tcp --dport 53 -j ACCEPT
186186+iptables -A INPUT -i lo -j ACCEPT
187187+iptables -A OUTPUT -o lo -j ACCEPT
188188+189189+# Create ipset with CIDR support
190190+ipset create allowed-domains hash:net
191191+192192+# GitHub IP ranges
193193+echo "Fetching GitHub IP ranges..."
194194+gh_ranges=$(curl -s --connect-timeout 10 --max-time 30 --fail https://api.github.com/meta)
195195+[ -z "$gh_ranges" ] && {
196196+ echo "ERROR: Failed to fetch GitHub IP ranges"
197197+ exit 1
198198+}
199199+echo "$gh_ranges" | jq -e '.web and .api and .git' >/dev/null ||
200200+ {
201201+ echo "ERROR: GitHub API response missing required fields"
202202+ exit 1
203203+ }
204204+205205+while read -r cidr; do
206206+ [[ "$cidr" =~ ^[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}/[0-9]{1,2}$ ]] ||
207207+ {
208208+ echo "ERROR: Invalid CIDR: $cidr"
209209+ exit 1
210210+ }
211211+ echo "Adding GitHub range $cidr"
212212+ ipset add allowed-domains "$cidr" -exist
213213+done < <(echo "$gh_ranges" | jq -r '(.web + .api + .git)[]' | grep -v ':' | aggregate -q)
214214+215215+# Extract Docker's actual DNS port so dnsmasq can connect to it directly.
216216+# Docker's embedded DNS listens on a random high port and uses iptables NAT to
217217+# redirect 127.0.0.11:53 → 127.0.0.11:<port>. By pointing dnsmasq at the real
218218+# port, its upstream queries naturally bypass our port-53 DNAT rules (no
219219+# uid-owner tricks needed) and don't require the DOCKER_OUTPUT NAT chain.
220220+DOCKER_DNS_PORT=$(echo "$DOCKER_DNS_RULES" | sed -n 's/.*udp.*--to-destination 127\.0\.0\.11:\([0-9]*\).*/\1/p' | head -1)
221221+[ -z "$DOCKER_DNS_PORT" ] && {
222222+ echo "ERROR: Failed to extract Docker DNS port"
223223+ exit 1
224224+}
225225+echo "Docker DNS port: $DOCKER_DNS_PORT"
226226+227227+# Configure dnsmasq to dynamically populate the ipset as domains are resolved.
228228+# This means the ipset always contains current IPs regardless of CDN rotation —
229229+# the IP is added to the ipset the moment it is resolved, before the connection
230230+# is made, so no connection is ever blocked due to stale IPs.
231231+# Add your agent's provider domain here (e.g. anthropic.com, api.openai.com).
232232+cat >/etc/dnsmasq.d/firewall-ipset.conf <<EOF
233233+# Forward all queries to Docker's embedded DNS (actual port, not 53)
234234+server=127.0.0.11#${DOCKER_DNS_PORT}
235235+# Listen only on an alternate loopback address to avoid conflicting with
236236+# any existing resolver on 127.0.0.1
237237+listen-address=127.0.0.2
238238+bind-interfaces
239239+# Populate the allowed-domains ipset with every IP returned when resolving
240240+# any of these domains (or their subdomains)
241241+ipset=/github.com/registry.npmjs.org/packagist.org/cdn.jsdelivr.net/marketplace.visualstudio.com/vscode.blob.core.windows.net/update.code.visualstudio.com/allowed-domains
242242+EOF
243243+244244+# Ensure idempotency when run as a postStartCommand: stop any existing dnsmasq.
245245+pkill dnsmasq 2>/dev/null || true
246246+247247+dnsmasq --conf-dir=/etc/dnsmasq.d
248248+echo "dnsmasq started"
249249+250250+# Redirect all outbound DNS queries (port 53) through dnsmasq so it can
251251+# populate the ipset before each connection. dnsmasq's own upstream queries
252252+# go to Docker's actual DNS port (not 53), so they are unaffected by these rules.
253253+iptables -t nat -I OUTPUT -p tcp --dport 53 -j DNAT --to-destination 127.0.0.2:53
254254+iptables -t nat -I OUTPUT -p udp --dport 53 -j DNAT --to-destination 127.0.0.2:53
255255+256256+# Allow traffic to/from the host gateway IP
257257+HOST_IP=$(ip route | grep default | cut -d" " -f3)
258258+[ -z "$HOST_IP" ] && {
259259+ echo "ERROR: Failed to detect host IP"
260260+ exit 1
261261+}
262262+echo "Host gateway IP: $HOST_IP"
263263+iptables -A INPUT -s "$HOST_IP" -j ACCEPT
264264+iptables -A OUTPUT -d "$HOST_IP" -j ACCEPT
265265+266266+# Allow inbound connections to published service ports (HTTP/HTTPS)
267267+iptables -A INPUT -p tcp --dport 80 -j ACCEPT
268268+iptables -A INPUT -p tcp --dport 443 -j ACCEPT
269269+iptables -A INPUT -p udp --dport 443 -j ACCEPT
270270+271271+# Default DROP policies
272272+iptables -P INPUT DROP
273273+iptables -P FORWARD DROP
274274+iptables -P OUTPUT DROP
275275+276276+# Allow established connections
277277+iptables -A INPUT -m state --state ESTABLISHED,RELATED -j ACCEPT
278278+iptables -A OUTPUT -m state --state ESTABLISHED,RELATED -j ACCEPT
279279+280280+# Allow outbound traffic to local/private networks (e.g., Docker Compose services
281281+# and a model running on the host via host.docker.internal)
282282+iptables -A OUTPUT -d 10.0.0.0/8 -j ACCEPT
283283+iptables -A OUTPUT -d 172.16.0.0/12 -j ACCEPT
284284+iptables -A OUTPUT -d 192.168.0.0/16 -j ACCEPT
285285+286286+# Allow outbound to whitelisted IPs only (GitHub CIDRs + IPs populated by dnsmasq)
287287+iptables -A OUTPUT -m set --match-set allowed-domains dst -j ACCEPT
288288+289289+# Reject everything else with immediate feedback
290290+iptables -A OUTPUT -j REJECT --reject-with icmp-admin-prohibited
291291+292292+echo "Firewall configuration complete"
293293+294294+# Verify
295295+if curl --connect-timeout 5 https://example.com >/dev/null 2>&1; then
296296+ echo "ERROR: Firewall check failed — able to reach example.com"
297297+ exit 1
298298+else
299299+ echo "OK: example.com is blocked"
300300+fi
301301+302302+if ! curl --connect-timeout 5 https://api.github.com/zen >/dev/null 2>&1; then
303303+ echo "ERROR: Firewall check failed — unable to reach api.github.com"
304304+ exit 1
305305+else
306306+ echo "OK: api.github.com is reachable"
307307+fi
308308+```
309309+310310+### 4. Run the firewall on container start
311311+312312+Add a `postStartCommand` to `.devcontainer/devcontainer.json`:
313313+314314+```jsonc
315315+{
316316+ "postStartCommand": "sudo /app/.devcontainer/init-firewall.sh",
317317+}
318318+```
319319+320320+### 5. Enable autonomous mode
321321+322322+With the sandbox in place, you can let the agent skip confirmation prompts. For Claude Code, add
323323+these settings to `customizations.vscode.settings` in `.devcontainer/devcontainer.json`:
324324+325325+```json
326326+{
327327+ "claudeCode.allowDangerouslySkipPermissions": true,
328328+ "claudeCode.initialPermissionMode": "bypassPermissions"
329329+}
330330+```
331331+332332+Or from the terminal:
333333+334334+```console
335335+claude --dangerously-skip-permissions
336336+```
337337+338338+The default allowlist covers GitHub, Packagist, the npm registry, jsDelivr, and the Visual Studio Code
339339+marketplace. Add any domain your agent needs (its provider API, a private registry) to the
340340+`ipset=` line in the script, then rebuild the container:
341341+342342+```bash
343343+# Domains are '/'-separated, ending with the ipset name
344344+ipset=/github.com/.../your-domain.com/allowed-domains
345345+```
346346+347347+## Without Visual Studio Code
348348+349349+The Dev Container configuration works with any tool that supports the
350350+[Dev Container specification](https://containers.dev/), including:
351351+352352+- [Dev Container CLI](https://github.com/devcontainers/cli) (`devcontainer up`)
353353+- [GitHub Codespaces](https://github.com/features/codespaces)
354354+- JetBrains IDEs (with the Dev Containers plugin)
355355+356356+The `customizations.vscode.extensions` entries only apply to Visual Studio Code. In other
357357+editors the agent CLI still installs from `postCreateCommand`; run it from the integrated
358358+terminal, or install the editor's own extension if one exists.
359359+360360+## Troubleshooting
361361+362362+### The firewall blocks a required domain
363363+364364+If the agent, Composer, or npm fails to reach a service, add the domain to the `ipset=` line in
365365+`.devcontainer/init-firewall.sh` and rebuild the container.
366366+367367+### The container fails to start
368368+369369+Ensure Docker is running and that you have allocated enough resources (at least 2 GB of RAM).
370370+The firewall requires the `NET_ADMIN` capability, granted in
371371+`.devcontainer/compose.devcontainer.yaml`.
+61
application/docs/alpine.md
···11+# Using Alpine Linux Instead of Debian
22+33+By default, Symfony Docker uses Debian-based FrankenPHP Docker images.
44+This is the recommended solution.
55+66+Alternatively, it's possible to use Alpine-based images, which are smaller but
77+are known to be slower, and have several known issues.
88+99+To switch to Alpine-based images, apply the following changes to the `Dockerfile`:
1010+1111+<!-- markdownlint-disable MD010 -->
1212+1313+```diff
1414+-FROM dunglas/frankenphp:1-php8.5 AS frankenphp_upstream
1515++FROM dunglas/frankenphp:1-php8.5-alpine AS frankenphp_upstream
1616+1717+-SHELL ["/bin/bash", "-euxo", "pipefail", "-c"]
1818++SHELL ["/bin/ash", "-euxo", "-c"]
1919+2020+-# hadolint ignore=DL3008
2121+-RUN <<-EOF
2222+- apt-get update
2323+- apt-get install -y --no-install-recommends \
2424+- file \
2525+- git
2626++# hadolint ignore=DL3018
2727++RUN <<-EOF
2828++ apk add --no-cache \
2929++ file \
3030++ git
3131+ install-php-extensions \
3232+3333+-# hadolint ignore=DL3008,SC3054,DL4006
3434+-RUN <<-'EOF'
3535+- apt-get update
3636+- apt-get install -y --no-install-recommends libtree
3737++# hadolint ignore=DL3018,SC3054,DL4006
3838++RUN <<-'EOF'
3939++ apk add --no-cache libtree
4040+ mkdir -p /tmp/libs
4141+- BINARIES=(frankenphp php file)
4242+- for target in $(printf '%s\n' "${BINARIES[@]}" | xargs -I{} which {}) \
4343++ BINARIES="frankenphp php file"
4444++ for target in $(printf '%s\n' $BINARIES | xargs -I{} which {}) \
4545+4646+- libtree -pv "$target" 2>/dev/null | grep -oP '(?:── )\K/\S+(?= \[)' | while IFS= read -r lib; do
4747++ libtree -pv "$target" 2>/dev/null | sed -n 's/.*── \(\/[^ ]*\) \[.*/\1/p' | while IFS= read -r lib; do
4848+4949+- rm -rf /var/lib/apt/lists/*
5050+5151+-FROM debian:13-slim AS frankenphp_prod
5252++FROM alpine:3 AS frankenphp_prod
5353+5454+-SHELL ["/bin/bash", "-euxo", "pipefail", "-c"]
5555++SHELL ["/bin/ash", "-euxo", "-c"]
5656+5757+-COPY --from=frankenphp_prod_builder /usr/lib/file/magic.mgc /usr/lib/file/magic.mgc
5858++COPY --from=frankenphp_prod_builder /usr/share/misc/magic.mgc /usr/share/misc/magic.mgc
5959+```
6060+6161+<!-- markdownlint-enable MD010 -->
application/docs/digitalocean-dns.png
This is a binary file and will not be displayed.
application/docs/digitalocean-droplet.png
This is a binary file and will not be displayed.
+81
application/docs/existing-project.md
···11+# Installing on an Existing Project
22+33+It's also possible to use Symfony Docker with existing projects!
44+55+First, [download this skeleton](https://github.com/dunglas/symfony-docker).
66+77+If you cloned the Git repository, be sure to not copy the `.git` directory
88+to prevent conflicts with the `.git` directory already in your existing project.
99+1010+You can copy the contents of the repository using Git and tar.
1111+This will not contain `.git` or any uncommitted changes.
1212+1313+```console
1414+git archive --format=tar HEAD | tar -xC my-existing-project/
1515+```
1616+1717+If you downloaded the skeleton as a ZIP you can just copy the extracted files:
1818+1919+```console
2020+cp -Rp symfony-docker/. my-existing-project/
2121+```
2222+2323+Enable the Docker support of Symfony Flex:
2424+2525+```console
2626+composer config --json extra.symfony.docker 'true'
2727+```
2828+2929+The [worker mode of FrankenPHP](https://frankenphp.dev/docs/worker/) is enabled by default.
3030+To use it with Symfony ≤ 7.3, install the FrankenPHP runtime:
3131+3232+```console
3333+composer require runtime/frankenphp-symfony
3434+```
3535+3636+Then update worker configuration:
3737+3838+<!-- markdownlint-disable MD010 -->
3939+4040+```diff
4141+ worker {
4242+ file ./public/index.php
4343++ env APP_RUNTIME Runtime\FrankenPhpSymfony\Runtime
4444+ {$FRANKENPHP_WORKER_CONFIG}
4545+ }
4646+```
4747+4848+<!-- markdownlint-enable MD010 -->
4949+5050+> [!TIP]
5151+>
5252+> You can disable worker mode by removing the `worker` directive from the `frankenphp`
5353+> global option in your `Caddyfile`.
5454+5555+Re-execute the recipes to update the Docker-related files according to
5656+the packages you use:
5757+5858+```console
5959+rm symfony.lock
6060+composer recipes:install --force --verbose
6161+```
6262+6363+Double-check the changes, revert the changes that you don't want to keep:
6464+6565+```console
6666+git diff
6767+```
6868+6969+Build the Docker images:
7070+7171+```console
7272+docker compose build --pull --no-cache
7373+```
7474+7575+Start the project!
7676+7777+```console
7878+docker compose up --wait
7979+```
8080+8181+Browse `https://localhost`, your Docker configuration is ready!
+25
application/docs/extra-services.md
···11+# Support for Extra Services
22+33+Symfony Docker is extensible.
44+When you install a compatible Composer package using Symfony Flex,
55+the recipe will automatically modify the `Dockerfile` and `compose.yaml` files
66+to fulfill the requirements of this package.
77+88+The currently supported packages are:
99+1010+- `symfony/orm-pack`: install a PostgreSQL service
1111+- `symfony/mercure-bundle`: use the Mercure.rocks module shipped with Caddy
1212+- `symfony/panther`: install Chromium and its drivers
1313+- `symfony/mailer`: install a Mailpit service
1414+- `blackfireio/blackfire-symfony-meta`: install a Blackfire service
1515+1616+> [!NOTE]
1717+>
1818+> If a recipe modifies the Dockerfile, the container needs to be rebuilt.
1919+2020+<!-- -->
2121+2222+> [!WARNING]
2323+>
2424+> We recommend that you use the `composer require` command inside the container
2525+> in development mode so that recipes can be applied correctly.
+129
application/docs/makefile.md
···11+# Makefile
22+33+Here is a Makefile template. It provides some shortcuts for the most common tasks.
44+To use it, create a new `Makefile` file at the root of your project. Copy/paste
55+the content in the template section. To view all the available commands, run `make`.
66+77+For example, in the [getting started section](/README.md#getting-started), the
88+`docker compose` commands could be replaced by:
99+1010+1. Run `make build` to build fresh images
1111+2. Run `make up` (detached mode without logs)
1212+3. Run `make down` to stop the Docker containers
1313+1414+Of course, this template is basic for now. But, as your application is growing,
1515+you will probably want to add some targets like running your tests as described
1616+in [the Symfony book](https://symfony.com/doc/current/the-fast-track/en/17-tests.html#automating-your-workflow-with-a-makefile).
1717+You can also find a more complete example in this [snippet](https://www.strangebuzz.com/en/snippets/the-perfect-makefile-for-symfony).
1818+1919+If you want to run make from within the `php` container, in the [Dockerfile](/Dockerfile),
2020+add:
2121+2222+<!-- markdownlint-disable MD010 -->
2323+2424+```diff
2525+ gettext \
2626+ git \
2727++ make \
2828+```
2929+3030+<!-- markdownlint-enable MD010 -->
3131+3232+And rebuild the PHP image.
3333+3434+> [!NOTE]
3535+>
3636+> If you are using Windows, you have to install [chocolatey.org](https://chocolatey.org/)
3737+> or [Cygwin](http://cygwin.com) to use the `make` command.
3838+>
3939+> Check out this [StackOverflow question](https://stackoverflow.com/q/2532234/633864)
4040+> for more explanations.
4141+4242+## The Template
4343+4444+<!-- markdownlint-disable MD010 MD013 -->
4545+4646+```Makefile
4747+# Executables (local)
4848+DOCKER_COMP = docker compose
4949+5050+# Docker containers
5151+PHP_CONT = $(DOCKER_COMP) exec php
5252+5353+# Executables
5454+PHP = $(PHP_CONT) php
5555+COMPOSER = $(PHP_CONT) composer
5656+SYMFONY = $(PHP) bin/console
5757+5858+# Misc
5959+.DEFAULT_GOAL = help
6060+.PHONY : help build up start down logs sh composer vendor sf cc test
6161+6262+## —— 🎵 🐳 The Symfony Docker Makefile 🐳 🎵 ——————————————————————————————————
6363+help: ## Outputs this help screen
6464+ @grep -E '(^[a-zA-Z0-9\./_-]+:.*?##.*$$)|(^##)' $(MAKEFILE_LIST) | awk 'BEGIN {FS = ":.*?## "}{printf "\033[32m%-30s\033[0m %s\n", $$1, $$2}' | sed -e 's/\[32m##/[33m/'
6565+6666+## —— Docker 🐳 ————————————————————————————————————————————————————————————————
6767+build: ## Builds the Docker images
6868+ @$(DOCKER_COMP) build --pull --no-cache
6969+7070+up: ## Start the docker hub in detached mode (no logs)
7171+ @$(DOCKER_COMP) up --detach
7272+7373+start: build up ## Build and start the containers
7474+7575+down: ## Stop the docker hub
7676+ @$(DOCKER_COMP) down --remove-orphans
7777+7878+logs: ## Show live logs
7979+ @$(DOCKER_COMP) logs --tail=0 --follow
8080+8181+sh: ## Connect to the FrankenPHP container
8282+ @$(PHP_CONT) sh
8383+8484+bash: ## Connect to the FrankenPHP container via bash so up and down arrows go to previous commands
8585+ @$(PHP_CONT) bash
8686+8787+test: ## Start tests with phpunit, pass the parameter "c=" to add options to phpunit, example: make test c="--group e2e --stop-on-failure"
8888+ @$(eval c ?=)
8989+ @$(DOCKER_COMP) exec -e APP_ENV=test php bin/phpunit $(c)
9090+9191+9292+## —— Composer 🧙 ——————————————————————————————————————————————————————————————
9393+composer: ## Run composer, pass the parameter "c=" to run a given command, example: make composer c='req symfony/orm-pack'
9494+ @$(eval c ?=)
9595+ @$(COMPOSER) $(c)
9696+9797+vendor: ## Install vendors according to the current composer.lock file
9898+vendor: c=install --prefer-dist --no-dev --no-progress --no-scripts --no-interaction
9999+vendor: composer
100100+101101+## —— Symfony 🎵 ———————————————————————————————————————————————————————————————
102102+sf: ## List all Symfony commands or pass the parameter "c=" to run a given command, example: make sf c=about
103103+ @$(eval c ?=)
104104+ @$(SYMFONY) $(c)
105105+106106+cc: c=c:c ## Clear the cache
107107+cc: sf
108108+```
109109+110110+<!-- markdownlint-enable MD010 MD013 -->
111111+112112+## Adding and Modifying Jobs
113113+114114+Make sure to add this configuration to the [.editorconfig](/.editorconfig) file,
115115+so that it forces your editor to use tabs instead of spaces.
116116+117117+> [!NOTE]
118118+>
119119+> Makefiles are not compatible with spaces by default.
120120+121121+```.editorconfig
122122+123123+[Makefile]
124124+indent_style = tab
125125+126126+```
127127+128128+If you still want to use space, you can configure the prefix in the Makefile itself.
129129+See [this answer on Stack Exchange](https://retrocomputing.stackexchange.com/a/20303).
+107
application/docs/mysql.md
···11+# Using MySQL
22+33+The Docker configuration of this repository is extensible thanks to Flex recipes.
44+By default, the recipe installs PostgreSQL.
55+66+If you prefer to work with MySQL, follow these steps:
77+88+First, install the `symfony/orm-pack` package as described:
99+1010+```console
1111+docker compose exec php composer req symfony/orm-pack
1212+```
1313+1414+## Docker Configuration
1515+1616+Change the database image to use MySQL instead of PostgreSQL in `compose.yaml`:
1717+1818+<!-- markdownlint-disable MD013 -->
1919+2020+```diff
2121+ ###> doctrine/doctrine-bundle ###
2222+- image: postgres:${POSTGRES_VERSION:-16}-alpine
2323++ image: mysql:${MYSQL_VERSION:-8.0.32}
2424+ environment:
2525+- POSTGRES_DB: ${POSTGRES_DB:-app}
2626++ MYSQL_DATABASE: ${MYSQL_DATABASE:-app}
2727+ # You should definitely change the password in production
2828++ MYSQL_RANDOM_ROOT_PASSWORD: "true"
2929+- POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-!ChangeMe!}
3030++ MYSQL_PASSWORD: ${MYSQL_PASSWORD:-!ChangeMe!}
3131+- POSTGRES_USER: ${POSTGRES_USER:-app}
3232++ MYSQL_USER: ${MYSQL_USER:-app}
3333+ healthcheck:
3434+- test: ["CMD", "pg_isready", "-d", "${POSTGRES_DB:-app}", "-U", "${POSTGRES_USER:-app}"]
3535++ test: ["CMD", "mysqladmin" ,"ping", "-h", "localhost"]
3636+ timeout: 5s
3737+ retries: 5
3838+ start_period: 60s
3939+ volumes:
4040+- - database_data:/var/lib/postgresql/data:rw
4141++ - database_data:/var/lib/mysql:rw
4242+ # You may use a bind-mounted host directory instead, so that it is harder to accidentally remove the volume and lose all your data!
4343+- # - ./docker/db/data:/var/lib/postgresql/data:rw
4444++ # - ./docker/db/data:/var/lib/mysql:rw
4545+ ###< doctrine/doctrine-bundle ###
4646+```
4747+4848+<!-- markdownlint-enable MD013 -->
4949+5050+Depending on the database configuration,
5151+modify the environment in the same file at `services.php.environment.DATABASE_URL`:
5252+5353+```yaml
5454+DATABASE_URL: mysql://${MYSQL_USER:-app}:${MYSQL_PASSWORD:-!ChangeMe!}@database:3306/${MYSQL_DATABASE:-app}?serverVersion=${MYSQL_VERSION:-8.0.32}&charset=${MYSQL_CHARSET:-utf8mb4}
5555+```
5656+5757+Since we changed the port, we also have to define this in the `compose.override.yaml`:
5858+5959+```diff
6060+ ###> doctrine/doctrine-bundle ###
6161+ database:
6262+ ports:
6363+- - "5432"
6464++ - "3306"
6565+ ###< doctrine/doctrine-bundle ###
6666+```
6767+6868+Last but not least, we need to install the MySQL driver in `Dockerfile`:
6969+7070+```diff
7171+ ###> doctrine/doctrine-bundle ###
7272+-RUN install-php-extensions pdo_pgsql
7373++RUN install-php-extensions pdo_mysql
7474+ ###< doctrine/doctrine-bundle ###
7575+```
7676+7777+## Change Environment
7878+7979+Change the database configuration in `.env`:
8080+8181+```dotenv
8282+DATABASE_URL=mysql://${MYSQL_USER:-app}:${MYSQL_PASSWORD:-!ChangeMe!}@database:3306/${MYSQL_DATABASE:-app}?serverVersion=${MYSQL_VERSION:-8.0.32}&charset=${MYSQL_CHARSET:-utf8mb4}
8383+```
8484+8585+## Final steps
8686+8787+Rebuild the Docker environment:
8888+8989+```console
9090+docker compose down --remove-orphans && docker compose build --pull --no-cache
9191+```
9292+9393+Start the services:
9494+9595+```console
9696+docker compose up --wait
9797+```
9898+9999+Test your setup:
100100+101101+<!-- markdownlint-disable MD013 -->
102102+103103+```console
104104+docker compose exec php bin/console dbal:run-sql -q "SELECT 1" && echo "OK" || echo "Connection is not working"
105105+```
106106+107107+<!-- markdownlint-enable MD013 -->
+121
application/docs/options.md
···11+# Docker Build Options
22+33+You can customize the Docker build process using these environment variables.
44+55+> [!NOTE]
66+>
77+> All Symfony-specific environment variables are used only if no `composer.json`
88+> file is found in the project directory.
99+1010+## Selecting a Specific Symfony Version
1111+1212+Use the `SYMFONY_VERSION` environment variable to select a specific Symfony version.
1313+1414+For instance, use the following command to install Symfony 6.4:
1515+1616+On Linux:
1717+1818+```console
1919+SYMFONY_VERSION=6.4.* docker compose up --wait
2020+```
2121+2222+On Windows:
2323+2424+```console
2525+set SYMFONY_VERSION=6.4.* && docker compose up --wait&set SYMFONY_VERSION=
2626+```
2727+2828+<!-- markdownlint-disable MD010 -->
2929+3030+> [!NOTE]
3131+>
3232+> If you're using Symfony 7.3 or earlier with FrankenPHP in worker mode, you also need to follow these steps
3333+>
3434+> ```console
3535+> composer require runtime/frankenphp-symfony
3636+> ```
3737+>
3838+> Add `env APP_RUNTIME Runtime\FrankenPhpSymfony\Runtime` in the `frankenphp/Caddyfile` in the `worker` section.
3939+>
4040+> ```diff
4141+> worker {
4242+> file ./public/index.php
4343+> + env APP_RUNTIME Runtime\FrankenPhpSymfony\Runtime
4444+> {$FRANKENPHP_WORKER_CONFIG}
4545+> }
4646+> ```
4747+4848+<!-- markdownlint-enable MD010 -->
4949+5050+## Installing Development Versions of Symfony
5151+5252+To install a non-stable version of Symfony,
5353+use the `STABILITY` environment variable during the build.
5454+5555+The value must be [a valid Composer stability option](https://getcomposer.org/doc/04-schema.md#minimum-stability).
5656+5757+For instance, use the following command to use the development branch of Symfony:
5858+5959+On Linux:
6060+6161+```console
6262+STABILITY=dev docker compose up --wait
6363+```
6464+6565+On Windows:
6666+6767+```console
6868+set STABILITY=dev && docker compose up --wait&set STABILITY=
6969+```
7070+7171+## Using Custom HTTP Ports
7272+7373+Use the environment variables `HTTP_PORT`, `HTTPS_PORT` and/or `HTTP3_PORT`
7474+to adjust the ports to your needs, e.g.
7575+7676+```console
7777+HTTP_PORT=8000 HTTPS_PORT=4443 HTTP3_PORT=4443 docker compose up --wait
7878+```
7979+8080+to access your application on [https://localhost:4443](https://localhost:4443).
8181+8282+> [!NOTE]
8383+>
8484+> Let's Encrypt only supports the standard HTTP and HTTPS ports.
8585+> Creating a Let's Encrypt certificate for another port will not work,
8686+> you have to use the standard ports or to configure Caddy to use another provider.
8787+8888+## `Caddyfile` Options
8989+9090+You can also customize the `Caddyfile` by using the following environment variables
9191+to inject options block, directive or configuration.
9292+9393+> [!TIP]
9494+>
9595+> All the following environment variables can be defined in your `.env` file
9696+> at the root of the project to keep them persistent at each startup.
9797+9898+<!-- markdownlint-disable MD013 -->
9999+100100+| Environment variable | Description | Default value |
101101+| ------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------- |
102102+| `CADDY_GLOBAL_OPTIONS` | the [global options block](https://caddyserver.com/docs/caddyfile/options#global-options), one per line | |
103103+| `CADDY_EXTRA_CONFIG` | the [snippet](https://caddyserver.com/docs/caddyfile/concepts#snippets) or the [named-routes](https://caddyserver.com/docs/caddyfile/concepts#named-routes) options block, one per line | |
104104+| `CADDY_SERVER_EXTRA_DIRECTIVES` | the [`Caddyfile` directives](https://caddyserver.com/docs/caddyfile/concepts#directives) | |
105105+| `CADDY_SERVER_LOG_OPTIONS` | the [server log options block](https://caddyserver.com/docs/caddyfile/directives/log), one per line | |
106106+| `SERVER_NAME` | the server name or address | `localhost` |
107107+| `FRANKENPHP_CONFIG` | a list of extra [FrankenPHP global directives](https://frankenphp.dev/docs/config/#caddyfile-config), one per line | |
108108+| `FRANKENPHP_WORKER_CONFIG` | a list of extra [FrankenPHP worker directives](https://frankenphp.dev/docs/config/#caddyfile-config), one per line | |
109109+| `MERCURE_PUBLISHER_JWT_KEY` | the JWT key to use for publishers | |
110110+| `MERCURE_PUBLISHER_JWT_ALG` | the JWT algorithm to use for publishers | `HS256` |
111111+| `MERCURE_SUBSCRIBER_JWT_KEY` | the JWT key to use for subscribers | |
112112+| `MERCURE_SUBSCRIBER_JWT_ALG` | the JWT algorithm to use for subscribers | `HS256` |
113113+| `MERCURE_EXTRA_DIRECTIVES` | a list of extra [Mercure directives](https://mercure.rocks/docs/hub/config), one per line | |
114114+115115+<!-- markdownlint-enable MD013 -->
116116+117117+### Customizing the Server Name
118118+119119+```console
120120+SERVER_NAME="app.localhost" docker compose up --wait
121121+```
+140
application/docs/production.md
···11+# Deploying in Production
22+33+Symfony Docker provides Docker images and a Docker Compose definition optimized
44+for production usage.
55+In this tutorial, we will learn how to deploy our Symfony application
66+on a single server using Docker Compose.
77+88+## Preparing a Server
99+1010+To deploy your application in production, you need a server.
1111+In this tutorial, we will use a virtual machine provided by DigitalOcean,
1212+but any Linux server can work.
1313+1414+If you already have a Linux server with Docker Compose installed,
1515+you can skip straight to [the next section](#configuring-a-domain-name).
1616+1717+Otherwise, use [this affiliate link](https://m.do.co/c/5d8aabe3ab80)
1818+to get $100 of free credit, create an account, then click on "Create a Droplet".
1919+Then, click on the "Marketplace" tab under the "Choose an image" section
2020+and search for the app named "Docker".
2121+This will provision an Ubuntu server with the latest versions of Docker and
2222+Docker Compose already installed!
2323+2424+For test purposes, the cheapest plans will be enough,
2525+even though you might want at least 2GB of RAM to execute Docker Compose
2626+for the first time.
2727+For real production usage,
2828+you'll probably want to pick a plan in the "general purpose" section
2929+to fit your needs.
3030+3131+
3232+3333+You can keep the defaults for other settings, or tweak them according to your needs.
3434+Don't forget to add your SSH key or create a password
3535+then press the "Finalize and create" button.
3636+3737+Then, wait a few seconds while your Droplet is provisioning.
3838+When your Droplet is ready, use SSH to connect:
3939+4040+```console
4141+ssh root@<droplet-ip>
4242+```
4343+4444+## Configuring a Domain Name
4545+4646+In most cases, you'll want to associate a domain name with your site.
4747+If you don't own a domain name yet, you'll have to buy one through a registrar.
4848+4949+Then create a DNS record of type `A` for your domain name pointing
5050+to the IP address of your server:
5151+5252+```dns
5353+your-domain-name.example.com. IN A 207.154.233.113
5454+```
5555+5656+Example with the DigitalOcean Domains service ("Networking" > "Domains"):
5757+5858+
5959+6060+> [!NOTE]
6161+>
6262+> Let's Encrypt, the service used by default by Symfony Docker to automatically
6363+> generate a TLS certificate doesn't support using bare IP addresses.
6464+> Using a domain name is mandatory to use Let's Encrypt.
6565+6666+## Deploying
6767+6868+Copy your project on the server using `git clone`, `scp`, or any other tool
6969+that may fit your need.
7070+If you use GitHub, you may want to use [a deploy key](https://docs.github.com/en/free-pro-team@latest/developers/overview/managing-deploy-keys#deploy-keys).
7171+Deploy keys are also [supported by GitLab](https://docs.gitlab.com/user/project/deploy_keys/).
7272+7373+Example with Git:
7474+7575+```console
7676+git clone git@github.com:<username>/<project-name>.git
7777+```
7878+7979+Go into the directory containing your project (`<project-name>`),
8080+and start the app in production mode:
8181+8282+```console
8383+# Build fresh production image
8484+docker compose -f compose.yaml -f compose.prod.yaml build --pull --no-cache
8585+8686+# Start container
8787+SERVER_NAME=your-domain-name.example.com \
8888+APP_SECRET=ChangeMe \
8989+CADDY_MERCURE_JWT_SECRET=ChangeThisMercureHubJWTSecretKey \
9090+docker compose -f compose.yaml -f compose.prod.yaml up --wait
9191+```
9292+9393+Be sure to replace `your-domain-name.example.com` with your actual domain name
9494+and to set the values of `APP_SECRET`, `CADDY_MERCURE_JWT_SECRET`
9595+to cryptographically secure random values.
9696+9797+Your server is up and running, and a HTTPS certificate has been automatically
9898+generated for you.
9999+Go to `https://your-domain-name.example.com` and enjoy!
100100+101101+> [!CAUTION]
102102+>
103103+> Docker can have a cache layer, make sure you have the right build
104104+> for each deployment or rebuild your project with `--no-cache` option
105105+> to avoid cache issues.
106106+107107+## Disabling HTTPS
108108+109109+Alternatively, if you don't want to expose an HTTPS server but only an HTTP one,
110110+run the following command:
111111+112112+```console
113113+SERVER_NAME=:80 \
114114+APP_SECRET=ChangeMe \
115115+CADDY_MERCURE_JWT_SECRET=ChangeThisMercureHubJWTSecretKey \
116116+docker compose -f compose.yaml -f compose.prod.yaml up --wait
117117+```
118118+119119+## Deploying on Multiple Nodes
120120+121121+If you want to deploy your app on a cluster of machines, you can use [Docker Swarm](https://docs.docker.com/engine/swarm/stack-deploy/),
122122+which is compatible with the provided Compose files.
123123+To deploy on Kubernetes, take a look
124124+at [the Helm chart provided with API Platform](https://api-platform.com/docs/deployment/kubernetes/),
125125+which can be easily adapted for use with Symfony Docker.
126126+127127+## Passing local environment variables to containers
128128+129129+By default, `.env.local` and `.env.*.local` files are excluded from production images.
130130+If you want to pass them to your containers, you can use the [`env_file` attribute](https://docs.docker.com/compose/how-tos/environment-variables/set-environment-variables/#use-the-env_file-attribute):
131131+132132+```yaml
133133+# compose.prod.yaml
134134+135135+services:
136136+ php:
137137+ env_file:
138138+ - .env.prod.local
139139+ # ...
140140+```
+88
application/docs/tls.md
···11+# TLS Certificates
22+33+## Trusting the Authority
44+55+With a standard installation, the authority used to sign certificates
66+generated in the Caddy container is not trusted by your local machine.
77+88+You must add the authority to the trust store of the host.
99+1010+<!-- markdownlint-disable MD013 -->
1111+1212+### Linux
1313+1414+```console
1515+docker cp $(docker compose ps -q php):/data/caddy/pki/authorities/local/root.crt /usr/local/share/ca-certificates/root.crt && sudo update-ca-certificates
1616+```
1717+1818+### Mac
1919+2020+```console
2121+docker cp $(docker compose ps -q php):/data/caddy/pki/authorities/local/root.crt /tmp/root.crt && sudo security add-trusted-cert -d -r trustRoot -k /Library/Keychains/System.keychain /tmp/root.crt
2222+```
2323+2424+### Windows
2525+2626+```console
2727+docker compose cp php:/data/caddy/pki/authorities/local/root.crt %TEMP%/root.crt && certutil -addstore -f "ROOT" %TEMP%/root.crt
2828+```
2929+3030+<!-- markdownlint-enable MD013 -->
3131+3232+## Using Custom TLS Certificates
3333+3434+By default, Caddy will automatically generate TLS certificates using Let's Encrypt
3535+or ZeroSSL.
3636+But sometimes you may prefer using custom certificates.
3737+3838+For instance, to use self-signed certificates created with [mkcert](https://github.com/FiloSottile/mkcert)
3939+do as follows:
4040+4141+1. Locally install `mkcert`
4242+2. Create the folder storing the certs:
4343+4444+ ```console
4545+ mkdir -p frankenphp/certs
4646+ ```
4747+4848+3. Generate the certificates for your local host (example: "server-name.localhost"):
4949+5050+ <!-- markdownlint-disable MD013 -->
5151+5252+ ```console
5353+ mkcert -cert-file frankenphp/certs/tls.pem -key-file frankenphp/certs/tls.key "server-name.localhost"
5454+ ```
5555+5656+ <!-- markdownlint-enable MD013 -->
5757+5858+4. Add these lines to the `./compose.override.yaml` file about `CADDY_SERVER_EXTRA_DIRECTIVES`
5959+ environment and volume for the `php` service:
6060+6161+ ```diff
6262+ php:
6363+ environment:
6464+ + CADDY_EXTRA_CONFIG: |
6565+ + https:// {
6666+ + tls /etc/caddy/certs/tls.pem /etc/caddy/certs/tls.key
6767+ + }
6868+ # ...
6969+ volumes:
7070+ + - ./frankenphp/certs:/etc/caddy/certs:ro
7171+ # ...
7272+ ```
7373+7474+5. Restart your `php` service
7575+7676+## Disabling HTTPS for Local Development
7777+7878+To disable HTTPS, configure your environment to use HTTP by setting the following
7979+variables and starting the project with this command:
8080+8181+```console
8282+SERVER_NAME=http://localhost \
8383+MERCURE_PUBLIC_URL=http://localhost/.well-known/mercure \
8484+docker compose up --wait
8585+```
8686+8787+Ensure your application is accessible over HTTP by visiting `http://localhost`
8888+in your web browser.
+53
application/docs/troubleshooting.md
···11+# Troubleshooting
22+33+## Editing Permissions on Linux
44+55+If you work on Linux and cannot edit some of the project files right after
66+the first installation, you can run the following command
77+to set yourself as owner of the project files that were created by the Docker container:
88+99+```console
1010+docker compose run --rm php chown -R $(id -u):$(id -g) .
1111+```
1212+1313+## TLS/HTTPS Issues
1414+1515+See the [TLS section](tls.md) for more details.
1616+1717+## Production Issues
1818+1919+### How To Properly Build Fresh Images for Production Use
2020+2121+Remember that, by default, if you run `docker compose up --wait`,
2222+only the files `compose.yaml` and `compose.override.yaml` will be used.
2323+See ["How Compose works"](https://docs.docker.com/compose/intro/compose-application-model)
2424+and ["Merge Compose files"](https://docs.docker.com/compose/how-tos/multiple-compose-files/merge).
2525+2626+If you need to build images for production environment, you have to use the following
2727+command:
2828+2929+```console
3030+docker compose -f compose.yaml -f compose.prod.yaml build --pull --no-cache
3131+```
3232+3333+### Building Dev and Prod Images
3434+3535+Dev and prod images use distinct image names (`app-php-dev` and `app-php-prod`),
3636+so they won't conflict with each other.
3737+3838+To build and start the dev image:
3939+4040+```console
4141+docker compose up --wait
4242+```
4343+4444+To build and start the prod image:
4545+4646+```console
4747+docker compose -f compose.yaml -f compose.prod.yaml build --pull --no-cache
4848+docker compose -f compose.yaml -f compose.prod.yaml up --wait
4949+```
5050+5151+> [!WARNING]
5252+>
5353+> The order of `-f` arguments matters.
+19
application/docs/updating.md
···11+# Updating Your Project
22+33+To import the changes made to the _Symfony Docker_ template into your project,
44+we recommend using [_template-sync_](https://github.com/coopTilleuls/template-sync):
55+66+1. Run the script to synchronize your project with the latest version of the skeleton:
77+88+ <!-- markdownlint-disable MD013 -->
99+1010+ ```console
1111+ curl -sSL https://raw.githubusercontent.com/coopTilleuls/template-sync/main/template-sync.sh | sh -s -- https://github.com/dunglas/symfony-docker
1212+ ```
1313+1414+ <!-- markdownlint-enable MD013 -->
1515+1616+2. Resolve conflicts, if any
1717+3. Run `git cherry-pick --continue`
1818+1919+For more advanced options, refer to [the documentation of _template sync_](https://github.com/coopTilleuls/template-sync#template-sync).
+98
application/docs/xdebug.md
···11+# Using Xdebug
22+33+The default development image is shipped with [Xdebug](https://xdebug.org/),
44+a popular debugger and profiler for PHP.
55+66+When using [Dev Containers](https://containers.dev/), Xdebug is pre-configured and works out of the box.
77+Open the **Run and Debug** panel in Visual Studio Code and start the **Debug PHP** launch configuration, then set your breakpoints and load a page.
88+99+For other setups, because it has a significant performance overhead, the step-by-step debugger
1010+is disabled by default.
1111+It can be enabled by including `debug` in the values of the `XDEBUG_MODE` environment variable.
1212+1313+On Linux and Mac:
1414+1515+```console
1616+XDEBUG_MODE=develop,debug docker compose up --wait
1717+```
1818+1919+On Windows:
2020+2121+```console
2222+set XDEBUG_MODE=develop,debug&& docker compose up --wait&set XDEBUG_MODE=
2323+```
2424+2525+## Debugging with Xdebug and PhpStorm
2626+2727+First, [create a PHP debug remote server configuration](https://www.jetbrains.com/help/phpstorm/creating-a-php-debug-server-configuration.html):
2828+2929+1. In the `Settings/Preferences` dialog, go to `PHP | Servers`
3030+2. Create a new server:
3131+ - Name: `symfony` (or whatever you want to use for the variable `PHP_IDE_CONFIG`)
3232+ - Host: `localhost` (or the one defined using the `SERVER_NAME` environment variable)
3333+ - Port: `443`
3434+ - Debugger: `Xdebug`
3535+ - Check `Use path mappings`
3636+ - Absolute path on the server: `/app`
3737+3838+You can now use the debugger!
3939+4040+1. In PhpStorm, open the `Run` menu and click on `Start Listening for PHP Debug Connections`
4141+2. Add the `XDEBUG_SESSION=PHPSTORM` query parameter to the URL of
4242+ the page you want to debug, or use [other available triggers](https://xdebug.org/docs/step_debug#activate_debugger)
4343+4444+ Alternatively, you can use [the **Xdebug extension**](https://xdebug.org/docs/step_debug#browser-extensions)
4545+ for your preferred web browser.
4646+4747+3. On the command line, we might need to tell PhpStorm which
4848+ [path mapping configuration](https://www.jetbrains.com/help/phpstorm/zero-configuration-debugging-cli.html#configure-path-mappings)
4949+ should be used, set the value of the PHP_IDE_CONFIG environment variable to
5050+ `serverName=symfony`, where `symfony` is the name of the debug server configured
5151+ above.
5252+5353+ Example:
5454+5555+ ```console
5656+ XDEBUG_SESSION=1 PHP_IDE_CONFIG="serverName=symfony" php bin/console ...
5757+ ```
5858+5959+## Debugging with Xdebug and Visual Studio Code
6060+6161+1. Install necessary [PHP extension for Visual Studio Code](https://marketplace.visualstudio.com/items?itemName=DEVSENSE.phptools-vscode).
6262+2. Add [debug configuration](https://code.visualstudio.com/docs/debugtest/debugging-configuration#_launch-configurations)
6363+ into your `.vscode\launch.json` file.
6464+6565+ Example:
6666+6767+ ```json
6868+ {
6969+ "version": "0.2.0",
7070+ "configurations": [
7171+ {
7272+ "name": "Debug PHP",
7373+ "type": "php",
7474+ "request": "launch",
7575+ "pathMappings": {
7676+ "/app": "${workspaceFolder}"
7777+ }
7878+ }
7979+ ]
8080+ }
8181+ ```
8282+8383+3. Use [Run and Debug](https://code.visualstudio.com/docs/debugtest/debugging#_start-a-debugging-session)
8484+ options and run `Debug PHP` to listen for upcoming connections
8585+ with [the **Xdebug extension**](https://xdebug.org/docs/step_debug#browser-extensions)
8686+ installed and active.
8787+8888+## Troubleshooting
8989+9090+Inspect the installation with the following command.
9191+The Xdebug version should be displayed.
9292+9393+```console
9494+$ docker compose exec php php --version
9595+9696+PHP ...
9797+ with Xdebug v3.x.x ...
9898+```
+66
application/frankenphp/Caddyfile
···11+{
22+ skip_install_trust
33+44+ {$CADDY_GLOBAL_OPTIONS}
55+66+ frankenphp {
77+ {$FRANKENPHP_CONFIG}
88+ }
99+}
1010+1111+{$CADDY_EXTRA_CONFIG}
1212+1313+{$SERVER_NAME:localhost} {
1414+ log {
1515+ {$CADDY_SERVER_LOG_OPTIONS}
1616+ # Redact the authorization query parameter that can be set by Mercure
1717+ format filter {
1818+ request>uri query {
1919+ replace authorization REDACTED
2020+ }
2121+ }
2222+ }
2323+2424+ root /app/public
2525+ encode zstd br gzip
2626+2727+ mercure {
2828+ # Publisher JWT key
2929+ publisher_jwt {env.MERCURE_PUBLISHER_JWT_KEY} {env.MERCURE_PUBLISHER_JWT_ALG}
3030+ # Subscriber JWT key
3131+ subscriber_jwt {env.MERCURE_SUBSCRIBER_JWT_KEY} {env.MERCURE_SUBSCRIBER_JWT_ALG}
3232+ # Allow anonymous subscribers (double-check that it's what you want)
3333+ anonymous
3434+ # Enable the subscription API (double-check that it's what you want)
3535+ subscriptions
3636+ # Extra directives
3737+ {$MERCURE_EXTRA_DIRECTIVES}
3838+ }
3939+4040+ vulcain
4141+4242+ {$CADDY_SERVER_EXTRA_DIRECTIVES}
4343+4444+ # Disable Topics tracking if not enabled explicitly: https://github.com/jkarlin/topics
4545+ header ?Permissions-Policy "browsing-topics=()"
4646+4747+ @phpRoute {
4848+ not path /.well-known/mercure*
4949+ not file {path}
5050+ }
5151+ rewrite @phpRoute index.php
5252+5353+ @frontController path index.php
5454+ php @frontController {
5555+ {$FRANKENPHP_SITE_CONFIG}
5656+5757+ worker {
5858+ file ./public/index.php
5959+ {$FRANKENPHP_WORKER_CONFIG}
6060+ }
6161+ }
6262+6363+ file_server {
6464+ hide *.php
6565+ }
6666+}
···11+; See https://docs.docker.com/desktop/features/networking/networking-how-tos/#connect-a-container-to-a-service-on-the-host
22+; See https://github.com/docker/for-linux/issues/264
33+; The `client_host` below may optionally be replaced with `discover_client_host=yes`
44+; Add `start_with_request=yes` to start debug session on each request
55+xdebug.client_host = host.docker.internal
···11+#!/bin/sh
22+set -e
33+44+if [ "$1" = 'frankenphp' ] || [ "$1" = 'php' ] || [ "$1" = 'bin/console' ]; then
55+66+ if [ -z "$(ls -A 'vendor/' 2>/dev/null)" ]; then
77+ composer install --prefer-dist --no-progress --no-interaction
88+ fi
99+1010+ # Display information about the current project
1111+ # Or about an error in project initialization
1212+ php bin/console -V
1313+1414+ if grep -q ^DATABASE_URL= .env; then
1515+ echo 'Waiting for database to be ready...'
1616+ ATTEMPTS_LEFT_TO_REACH_DATABASE=60
1717+ until [ $ATTEMPTS_LEFT_TO_REACH_DATABASE -eq 0 ] || DATABASE_ERROR=$(php bin/console dbal:run-sql -q "SELECT 1" 2>&1); do
1818+ if [ $? -eq 255 ]; then
1919+ # If the Doctrine command exits with 255, an unrecoverable error occurred
2020+ ATTEMPTS_LEFT_TO_REACH_DATABASE=0
2121+ break
2222+ fi
2323+ sleep 1
2424+ ATTEMPTS_LEFT_TO_REACH_DATABASE=$((ATTEMPTS_LEFT_TO_REACH_DATABASE - 1))
2525+ echo "Still waiting for database to be ready... Or maybe the database is not reachable. $ATTEMPTS_LEFT_TO_REACH_DATABASE attempts left."
2626+ done
2727+2828+ if [ $ATTEMPTS_LEFT_TO_REACH_DATABASE -eq 0 ]; then
2929+ echo 'The database is not up or not reachable:'
3030+ echo "$DATABASE_ERROR"
3131+ exit 1
3232+ else
3333+ echo 'The database is now ready and reachable'
3434+ fi
3535+3636+ if [ "$(find ./migrations -iname '*.php' -print -quit)" ]; then
3737+ php bin/console doctrine:migrations:migrate --no-interaction --all-or-nothing
3838+ fi
3939+ fi
4040+4141+ echo 'PHP app ready!'
4242+fi
4343+4444+exec docker-php-entrypoint "$@"
+9
application/public/index.php
···11+<?php
22+33+use App\Kernel;
44+55+require_once dirname(__DIR__).'/vendor/autoload_runtime.php';
66+77+return static function (array $context) {
88+ return new Kernel($context['APP_ENV'], (bool) $context['APP_DEBUG']);
99+};
application/src/Controller/.gitignore
This is a binary file and will not be displayed.
+19
application/src/Kernel.php
···11+<?php
22+33+namespace App;
44+55+use Symfony\Bundle\FrameworkBundle\Kernel\MicroKernelTrait;
66+use Symfony\Component\HttpKernel\Kernel as BaseKernel;
77+88+class Kernel extends BaseKernel
99+{
1010+ use MicroKernelTrait;
1111+1212+ /**
1313+ * @return list<string> An array of allowed values for APP_ENV
1414+ */
1515+ private function getAllowedEnvs(): array
1616+ {
1717+ return ['prod', 'dev', 'test'];
1818+ }
1919+}
···1111dagger --help
1212```
13131414+RSSBase uses Dagger's workspace/module config split:
1515+1616+- root [`../dagger.toml`](../dagger.toml) is the workspace config
1717+- [`../application/dagger-module.toml`](../application/dagger-module.toml) is the application module config
1818+- module code lives in [`../application/.dagger`](../application/.dagger)
1919+2020+From the repository root:
2121+2222+```bash
2323+# Inspect workspace modules
2424+dagger workspace
2525+dagger installed
2626+2727+# List workspace entrypoints; currently this exposes the application module
2828+dagger functions
2929+3030+# List and run checks declared with @check
3131+dagger check --list
3232+dagger check
3333+3434+# Build the production FrankenPHP image
3535+dagger call application build-prod sync
3636+3737+# Validate Composer metadata
3838+dagger call application composer-validate
3939+```
4040+4141+Tip: set `DAGGER_NO_NAG=1` to hide the Dagger Cloud setup prompt in local runs:
4242+```bash
4343+DAGGER_NO_NAG=1 dagger call application php-version
4444+```
4545+1446## Updating
15471648This package follows Dagger's dev-build endpoint, but pins a concrete commit and hashes for reproducibility.