Publishing your own Docker Image is easy!
Last week since writing this blog article I decided it is time to make my own Docker Image for Apie. The idea is simple: the docker image install the framework with Apie configured and all you need to as a developer is mount a folder for the application specific domain objects and you are free to go!
So how to publish your own docker image?
So what is a published Docker image?
Before making our own public Docker Image you should know what a published Docker image is. A Docker image is nothing more than a list of Docker layers. Any of these layers is stored individually and contains a list of changed files and folders. The layers are stacked on eacher other: so the last layer can override files in the previous Docker layer.
When you build a Docker image from a dockerfile, every command can make a new Docker layer.
Publishing a Docker image means that you upload all these layers to a Docker repository. The most common Docker repositories are the one used by Github and the official one on Dockerhub, but also from Amazon, Redhat and Gitlab.
For this blog article I decided to use the official one on Dockerhub.
Building a docker image
The best step to start with is putting everyting in a git repository and have a dockerfile in the root of the project. We want to make multiple variations of your docker images. We can do this with build arguments. We want to make different images for different PHP versions and we want to have a FPM and a CLI image. We end up with this initial dockerfile setup:
# Build arguments (global)
ARG PHP_VERSION=8.3
ARG PHP_VARIANT=cli
# Use base image
FROM php:${PHP_VERSION}-${PHP_VARIANT}
# Redeclare ARGs in this build stage
ARG PHP_VERSION
ARG PHP_VARIANT
# Pass build args to ENV
ENV PHP_VERSION=${PHP_VERSION}
ENV PHP_VARIANT=${PHP_VARIANT}
ENV FRAMEWORK=symfony
Why the double ARG? Well the moment you use FROM any build argument variable is lost and you have to redeclare them to get them back. We also can not skip the first one as we pick a different PHP version depending on the variant argument. If we do not provide an argument, it will pick php 8.3 with cli.
To make a specific image variation we need to provide the arguments when you run docker build (in this case fpm build with php 8.4:
docker build --build-arg PHP_VERSION=8.4 --build-arg PHP_VARIANT=fpm -t my-symfony-app:8.4-fpm .
The FROM part gets the docker layers from the php docker image that is also uploaded on Dockerhub. So how do we get our own code in it? The easiest solution is using COPY in the dockerfile and copy a complete folder into the docker image.
# Set the working directory
WORKDIR /app
# Copy only the selected framework's composer files first (for layer caching)
COPY symfony/composer.json composer.json
COPY symfony/composer.lock composer.lock
# Install Composer dependencies without scripts yet
RUN composer install --no-scripts --no-autoloader --prefer-dist --no-interaction
# Copy the rest of the selected framework's code
COPY symfony/ ./
So
COPY composer.json
, COPY composer.lock
and RUN composer install
all create a new docker layer. With COPY symfony/ ./
all contents of the symfony folder excluding files in .dockerignore will be copied into the image. If I make a change in the symfony folder and do docker build it wil see the changes and only apply changes to this. If the base php image is being updated it will rebuild everything. The only workaround so it becomes more stable you need to provide the EXACT version you require. This appproach's downside is that you will always have to manually update the dependencies yourself, but it prevents you from the classical "it works on my machine".The same can also be said about whether you make many small layers or one big layer of everything.
This is more optimized for Docker to run as it creates one big Docker layer:
RUN apt install bzip && apt install example && apt install php
But this is more optimized for making changes in the dockerfile and rebuilding it (assuming the last part will be changed more often):
RUN apt install bzip
RUN apt install example
RUN apt install php
I think writing everything as multiple RUN commands for testing and once you are done make it compact to optimize for Docker.
Publishing the image
So now we have a docker image we can build locally, but we want to be able to publish it locally so everybody can use it effortlessly. This is very simple with Github actions. Since we are using Github actions all we need is a workflow file inside .github/workflow folder:
name: Build and Push Docker Images
on:
push:
branches:
- main
workflow_dispatch:
jobs:
build:
runs-on: ubuntu-latest
strategy:
matrix:
framework: [laravel, symfony]
php_variant: [cli, fpm]
php_version: [8.3, 8.4]
steps:
- name: Checkout code
uses: actions/checkout@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Log in to Docker Hub
uses: docker/login-action@v3
with:
username: apieprogrammer
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Build and push Docker image
uses: docker/build-push-action@v5
with:
context: .
file: dockerfile.${{ matrix.framework }}
push: true
build-args: |
PHP_VERSION=${{ matrix.php_version }}
PHP_VARIANT=${{ matrix.php_variant }}
tags: |
apieprogrammer/apie:${{ matrix.framework }}-${{ matrix.php_variant }}-${{ matrix.php_version }}
So the matrix strategy is the way to make 8 Docker image variations at the same time. PHP is available in 2 versions: cli and fpm. Fpm is used in combination with a webserver like nginx or apache for improving speed of running a PHP process pool for handling requests. Cli can run from the terminal, but is less optimized for usage with apache and nginx. By default Github actions will run the steps with every combination of the matrix:
framework | php_variant | php_version |
---|---|---|
laravel | cli | 8.3 |
symfony | cli | 8.3 |
laravel | fpm | 8.3 |
symfony | fpm | 8.3 |
laravel | cli | 8.4 |
symfony | cli | 8.4 |
laravel | fpm | 8.4 |
symfony | fpm | 8.4 |
In the steps we install docker buildx for being able to publish images. We login with docker/login-action to get permission to upload the image. With docker/build-push-action we build the image and publish it wisth a tag name. As you can see running the github actions updates the image with the same tag. The previous version is still accessible from the container digest, but the tag is now pointing to a new container digest after publishing.
This means that if you already have it installed you get an old version because Docker caches these results, but rebuilding a dockerfile with FROM or pulling the docker images again will update the image to the later version. The only way to avoid changes between users is publishing the images always with a new version name. But to me this felt more like overkill then actual good usage of your Docker images. It is also harder to generate version numbers. You can see the current digest on Dockerhub itself:
Now you can use the image like you can use any Docker image including mounting volumes for your application. In the case of Apie I can mount it to a domains folder to build an Apie application without having to maintain the Apie library code. The only downside is that you will need to configure your IDE to read from the container or make a dummy composer install to get autocompletion.
So to use Apie within a Symfony application without Nginx and Apache on PHP 8.3 I can use it like this:
services:
app:
image: apieprogrammer/apie:symfony-cli-8.3
container_name: myapp
environment:
APP_ENV: production
ports:
- "8000:8000"
volumes:
- ./domains:/app/domains
networks:
- application
networks:
application:
Conclusion
Publishing your own Docker image is not hard. If you can build a docker image locally, you can also publish it. Make sure you do come up what variations you want to offer. I was suprised how easy it was.
Comments
Post a Comment