Do you know that feeling of:
- ...coming back to a project after a year and not knowing how to run it?
- ...reinventing the-same-but-slightly-different wheel for every small app?
- ...rewriting the same few paragraph of documentation for every tool you built?
- ...spending 2h on coding a solution for your problem, and the next 20h to prepare to share it?
Yeah, me too ;) This repository is an attempt to solve this.
- Clone/download this repository to a separate folder
- Copy files from
app-skeletoninto your project. Merge any changes. - Search for
# todo:to find any placeholders that should be changed for your individual project. - Type
maketo see commands and enjoy your new build system :)
You probably don't need to modify anything else, even indocker-composefiles.
This repository is deliberately structured in a monorepo fashion, thus not supporting GitHub/GitLab template paradigm. In the future it will contain more examples.
This repository serves as an easy-start for bootstrapping deployment & development scaffolding for new PHP apps, or updating older ones. It contains a good base for any app, that can later be customized. The intention is to have a good opinionated toolset that can be customized by removing parts that aren't needed [yet]. The key design goal here is to have "all but the kitchen sink" but built in a way that is easily customizable.
- Use modern, performant, yet flexible components
- Provide base for web-only and web-cli hybrid projects
- Provide base for cli-only apps, that isn't compromised by web-only assumptions
- Ability to easily build for multiple architectures
- Ensure builds can support both lightweight Alpine Linux and fully-featured Debian
- Provide templates of documentation and unified user & developer's experience across projects
It is said to be opinionated, which is a keyword for "it's a base I think makes sense". It strictly stays away from providing multiple ways of doing the same thing. For example, it intentionally does not include configuration for nginx+FPM instead of Caddy, or support for projects without Git. This is to reduce complexity and make later updates easier in a project where there isn't a development team behind the scenes :)
Although the base by-design isn't infinitely flexible, it bends the rules in some places to offer a better DX. For
example, it works without composer.json, or it doesn't enforce (but supports) post-build pre-deploy build command.
The project employs certain conventions that shouldn't be changed on per-project basis:
- Orchestration/build/chore-type tasks use Makefile
- Many PHP developers dislike GNU
makedue to its complexity, but Docker native tooling is often insufficient (1, 2, 3) - Even Docker official orchestration hacks around limitations of their own tooling
- In case of PHP apps, Makefiles are more like a self-documenting shell script, rather than an integral part of a build+code pipeline.
- The alternative would be using an overcomplicated shell script, which would probably be worse than using make :)
- Many PHP developers dislike GNU
- App-related commands go into
composer.jsonscripts- To follow standard PHP conventions, anything code-related is encapsulated in
composer.json - The convention ensures any new developer joining the project doesn't need to learn new tooling
- To follow standard PHP conventions, anything code-related is encapsulated in
This repository was designed to abstract complexity of multi-stage, multi-OS, and multi-architecture builds & releases, so that even small project can offer a high-quality experience for everyone. How the build & run process works depends on the audience:
- End-users of the application
- Regular users are encouraged to grab
docker-compose.yamland customize it as-needed. This assumption was made due to several factors:- Technical users who aren't developers (e.g. sysadmins) often find git workflows confusing
- Private Git repositories are often inaccessible from production server
- It's unrealistic to expect
docker-compose.yamlto be identical across multiple environments (e.g. networks)
- End users are intended to use
docker compose up, or one of their favorite GUI-based tools like Komodo/Portainer/NAS solutions. Thus, it also involves no image build process, as users are meant to get pre-built images from a registry. This is to limit number of moving parts, reducing possibility of environment-related crashes, shifting any issues into CI/dev build phase. - Variables set by users in
environment:are only these that application exposes, with defaults in.envand maybe for web scenarios with edge cases some fromCaddyfile. This is to makedocker-compose.yamlfile lean. This reduces maintenance burden for developer of the application.
- Regular users are encouraged to grab
- Daily development
- Developers are intended to use
docker-compose.override.yamlthat extendsdocker-compose.yaml, ensuring the development execution environment is as close to what users run as possible. - The developer's stack mounts not only code, but also all configs via bind mount, while building a single dev image for current architecture and default OS specified. This allows for easy onboarding of new developers and makes it easy to come back to that project after 6 months.
- Developers can set any
.env&Caddyfilevariables, but also overwrite any built-in withENVinDockerfile - The tooling provided in
Makefileand automations indocker-entrypoint.shabstract common annoyances related to permissions. Again, this ensures that developers use a similar security context as the production version, rather than running as root "because it's just for development". It also attempts to detect common user & developer's pitfalls.
- Developers are intended to use
- Releasing version
- The release process is often very complex and follows a completely different pathways than what even developers use. The complexity increases severely when multiple-OS & multiple-arch builds are desired. This is what this repository tries to solve.
- The build process flows in layers:
Makefile=>docker-bake.hcl=>Dockerfile=> OCI images - The base of all build-time variables is
build/build-env.sh, providing unified environment for both dev & prod images, preventing prod-only bugs - This repository includes some templates for documentation that can be included verbatim with the application
Looking at app-skeleton you may wonder "Why are there so many tools used?!". This is exactly why this project
exists. It has been extracted & abstracted from an application repository after many attempts to provide a good
developer's experience, combined with providing a solid & easy to use base for users. Below is a list of decisions made
along the way and the reasoning behind them, but TL;DR is none of the elements included are present to be more fancy
but to achieve a goal in a robust manner.
- Combined web+cli Dockerfile
- Managing multiple Dokcerfiles is often challenging, as it leads to diverging behaviors between cli/web over time, increasing maintenance burden.
- PHP applications often start as CLI-only, but often get an API backend in the process. Including a combined Dockerfile makes this transition easier.
- Support for Alpine & Debian
- In general, the community prefers Alpine Linux, due to smaller footprint increasing security while also offering perceived lightness. Thus, from the perspective of end users of the application it's beneficial to pick Alpine.
- In the other hand, due to usage of
muslinstead ofglibc, performance of applications running under Alpine is often much lower in comparison to Debian. In addition, a lot of bugs crop up in (not only) PHP. - For details check long comment in the Dockerfile.
- Multi-arch builds
- Both Amazon Graviton and Apple M-series CPUs brought ARM64 to the masses. The AMD64 architecture remains popular as well. Thus, making multi-arch builds is pretty much a standard practice nowadays.
build.env.sh- A proper build practice is to mark builds with some reference to a version control system. In all cases, this requires executing a command somewhere in the process. Docker Bake does not support that behavior out of the box.
- This simple shell script allows for setting a uniform set of variables, with some of them being accessible to the application itself.
- The script being used both for dev & prod environment unifies and simplifies the development experience.
docker-bake.hcl- Docker Bake greatly simplifies multi-arch & multi-platform builds. However, the supported feature set is still quite limited.
- While Docker Bake supports
compose.yamlsyntax the syntax is very limited in comparison to HCL. For example, automatic tagging would not be possible using just the YAML syntax.
- FrankenPHP
- There's nothing wrong with the classic pairing of nginx+PHP-FPM. The new generation of application runtimes/servers like FrankenPHP, Swoole, or Roadrunner allow for much higher performance and often simpler configuration.
- While many solutions require at least some changes to the application and/or the development practices to prevent memory leaks, FrankenPHP does not when running in classic mode. This allows for progressive upgrade of existing applications.
- FrankenPHP gained an official support from The PHP Foundation signaling being the potential recommended direction for modern PHP apps.
- Being built on top of Caddy, it is also an excellent web server for static content, with first-class HTTPS support.
Makefile- Offering Makefile allows for very simple unified command set, like
make compose-update, even if the "backend" command set changes. This allows for more flexibility across multiple projects, while keeping developer's experience unified. - Utilizing
Makefileavoids common pitfalls, e.g. baredocker compose execignoring entrypoint safeguarding permissions. - While GNU Make can be complex, in this skeleton it serves more a role of a "standardized script" than a part of the actual build system.
- Offering Makefile allows for very simple unified command set, like
- Folder structure for PHP.ini files instead of a single php.ini
- Using a folder structure allows for flexibility of extending the application configuration without directly editing
template-provided
php.inifiles. - Due to non-recursive directory loading behavior it greatly simplifies Dockerfile build process and limits possibilities of prod & dev environments diverging.
- Using a folder structure allows for flexibility of extending the application configuration without directly editing
template-provided
- Complex
docker-entrypoint.sh- The entrypoint script complexity stems mainly from Docker's inflexibility of
--user/user:directive. Managing permissions in such scenarios, without running containerized applications as root, is often difficult. This results in many projects resorting to simply using root for convince, or relying on end-users to manage permissions. This template aims to offer a good security-minded base, without compromising DX/UX. - Based on experience working withing various teams, the script attempts to prevent/warn about common mistakes.
- The entrypoint offered in the template specifically aims to be identical in dev & prod environments and for both cli & web apps.
- The entrypoint script complexity stems mainly from Docker's inflexibility of
.editorconfig- The included EditorConfig does not set any standards, beyond what is required for proper operations of the toolset. For example, a common issue is to debug Makefiles that by accident use spaces, which by default aren't supported by GNU Make as indentation.
- Documentation (
.env&README.dev.md&README.md)- Most developers don't like writing documentation :) This is why this repository includes some premade/skeleton of the documentation, encouraging every project to have at least very basic getting started section.
See template-sync by Les-Tilleuls.coop.
This project has been inspired by Kévin Dunglas symfony-docker project.
