(#146) Re-wrote the docker code to generate a single self-contained d… (#153)

* (#146) Re-wrote the docker code to generate a single self-contained docker
image rather than using a docker-compose network of connected
containers

* (#146) Push version tags to docker hub automatically

* (#146) Switched to using a multistage docker build process to make the Dockerfile more readable and cache friendly without bloating the published image

* #146 More readable names

* #146 Documented the upgrade process and made 'artisan migrate' run on every boot to automate the upgrade process.
This commit is contained in:
Don Benjamin 2023-07-27 10:34:19 +01:00 committed by GitHub
parent 524d4db56e
commit 8f84faf3d1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 412 additions and 173 deletions

2
.dockerignore Normal file
View File

@ -0,0 +1,2 @@
.git
Dockerfile

View File

@ -1,9 +1,62 @@
POSTGRES_USER="postgres" APP_NAME="OpnForm"
POSTGRES_PASSWORD="postgres" APP_ENV=local
POSTGRES_DB="postgres" APP_KEY=
APP_DEBUG=false
APP_LOG_LEVEL=debug
APP_URL=http://localhost
DB_HOST="database" LOG_CHANNEL=errorlog
DB_DATABASE="postgres" LOG_LEVEL=debug
DB_USERNAME="postgres"
DB_PASSWORD="postgres" DB_CONNECTION=pgsql
DB_HOST=127.0.0.1
DB_PORT=5432 DB_PORT=5432
DB_DATABASE=postgres
DB_USERNAME=postgres
DB_PASSWORD=postgres
FILESYSTEM_DRIVER=s3
FILESYSTEM_DISK=s3
BROADCAST_DRIVER=log
CACHE_DRIVER=redis
QUEUE_CONNECTION=redis
SESSION_DRIVER=file
SESSION_LIFETIME=120
REDIS_HOST=127.0.0.1
REDIS_PASSWORD=null
REDIS_PORT=6379
MAIL_MAILER=smtp
MAIL_HOST=smtp.mailtrap.io
MAIL_PORT=2525
MAIL_USERNAME=null
MAIL_PASSWORD=null
MAIL_ENCRYPTION=null
MAIL_FROM_ADDRESS=null
MAIL_FROM_NAME="${APP_NAME}"
AWS_ACCESS_KEY_ID=
AWS_SECRET_ACCESS_KEY=
AWS_DEFAULT_REGION=us-east-1
AWS_BUCKET=
PUSHER_APP_ID=
PUSHER_APP_KEY=
PUSHER_APP_SECRET=
PUSHER_APP_CLUSTER=mt1
MIX_PUSHER_APP_KEY="${PUSHER_APP_KEY}"
MIX_PUSHER_APP_CLUSTER="${PUSHER_APP_CLUSTER}"
JWT_TTL=1440
JWT_SECRET=
STRIPE_KEY=
STRIPE_SECRET=
MUX_WORKSPACE_ID=
MUX_API_TOKEN=
OPEN_AI_API_KEY=

26
.github/workflows/dockerhub.yml vendored Normal file
View File

@ -0,0 +1,26 @@
name: Publish Docker image
on:
push:
tags:
- "v*"
jobs:
push_to_registry:
name: Push Docker image to Docker Hub
runs-on: ubuntu-latest
steps:
- name: Get tag name
run: ( echo "TAG_NAME=${GITHUB_REF#refs/*/v}"; echo "DOCKER_REPO=${{secrets.DOCKER_REPO}}") >> $GITHUB_ENV
- name: Check out the repo
uses: actions/checkout@v3
- name: Log in to Docker Hub
run: docker login -u "${{ secrets.DOCKER_USERNAME }}" -p "${{ secrets.DOCKER_ACCESS_TOKEN }}"
- name: Build docker image
run: docker build . -t $DOCKER_REPO:latest -t $DOCKER_REPO:$TAG_NAME
- name: Push Docker image
run: docker push $DOCKER_REPO:latest && docker push $DOCKER_REPO:$TAG_NAME

87
Dockerfile Normal file
View File

@ -0,0 +1,87 @@
ARG PHP_PACKAGES="php8.1 composer php8.1-common php8.1-pgsql php8.1-redis php8.1-mbstring\
php8.1-simplexml php8.1-bcmath php8.1-gd php8.1-curl php8.1-zip\
php8.1-imagick php8.1-bz2 php8.1-gmp php8.1-int php8.1-pcov php8.1-soap php8.1-xsl"
FROM node:16 AS javascript-builder
WORKDIR /app
# It's best to add as few files as possible before running the build commands
# as they will be re-run everytime one of those files changes.
#
# It's possible to run npm install with only the package.json and package-lock.json file.
ADD package.json package-lock.json ./
RUN npm install
ADD resources /app/resources
ADD public /app/public
ADD tailwind.config.js vite.config.js postcss.config.js /app/
RUN npm run build
# syntax=docker/dockerfile:1.3-labs
FROM --platform=linux/amd64 ubuntu:23.04 AS php-dependency-installer
ARG PHP_PACKAGES
RUN apt-get update \
&& apt-get install -y $PHP_PACKAGES composer
WORKDIR /app
ADD composer.json composer.lock artisan ./
# Running artisan requires the full php app to be installed so we need to remove the
# post-autoload command from the composer file if we want to run composer without
# adding a dependency to all the php files.
RUN sed 's_@php artisan package:discover_/bin/true_;' -i composer.json
RUN composer install
ADD app /app/app
ADD bootstrap /app/bootstrap
ADD config /app/config
ADD database /app/database
ADD public public
ADD routes routes
ADD tests tests
# Manually run the command we deleted from composer.json earlier
RUN php artisan package:discover --ansi
FROM --platform=linux/amd64 ubuntu:23.04
# supervisord is a process manager which will be responsible for managing the
# various server processes. These are configured in docker/supervisord.conf
CMD ["/usr/bin/supervisord"]
WORKDIR /app
ARG PHP_PACKAGES
RUN apt-get update \
&& apt-get install -y \
supervisor nginx sudo postgresql-15 redis\
$PHP_PACKAGES php8.1-fpm\
&& apt-get clean
ADD docker/postgres-wrapper.sh docker/php-fpm-wrapper.sh docker/redis-wrapper.sh /usr/local/bin/
ADD docker/php-fpm.conf /etc/php/8.1/fpm/pool.d/
ADD docker/nginx.conf /etc/nginx/sites-enabled/default
ADD docker/supervisord.conf /etc/supervisor/conf.d/supervisord.conf
ADD .env.docker .env
ADD . .
COPY --from=javascript-builder /app/public/build/ ./public/build/
COPY --from=php-dependency-installer /app/vendor/ ./vendor/
RUN chmod a+x /usr/local/bin/*.sh /app/artisan \
&& ln -s /app/artisan /usr/local/bin/artisan \
&& useradd opnform \
&& echo "daemon off;" >> /etc/nginx/nginx.conf\
&& echo "daemonize no" >> /etc/redis/redis.conf\
&& echo "appendonly yes" >> /etc/redis/redis.conf\
&& echo "dir /persist/redis/data" >> /etc/redis/redis.conf
EXPOSE 80

View File

@ -1,43 +0,0 @@
up: down .env .make.composer-install .make.npm-install .make.npm-build .make.artisan-key-generate .make.jwt-secret .make.migrate server
down:
docker compose down
clean:
rm ./.make.*
server:
docker compose up -d server
logs:
docker compose logs -f server
.make.composer-install: composer.json composer.lock
docker compose run --rm composer-install
touch $@
.make.npm-install: package.json package-lock.json
docker compose run --rm npm-install
touch $@
.make.npm-build:
docker compose run --rm npm-build
touch $@
.make.artisan-key-generate:
docker compose run --rm php-cli artisan key:generate
touch $@
.make.jwt-secret:
docker compose run --rm php-cli artisan jwt:secret --force
touch $@
.make.migrate:
docker compose run --rm migrate
touch $@
.env:
cp .env.example .env
.PHONY: up down clean server

View File

@ -44,35 +44,84 @@ It takes 1 minute to try out the builder for free. You'll have high availability
### Docker installation 🐳 ### Docker installation 🐳
There's a `docker compose` setup automating most of the manual steps: There's a `Dockerfile` for building a self-contained docker image including databases, webservers etc.
```bash This can be built and run locally but is also hosted publicly on docker hub at `jhumanj/opnform` and is generally best run directly from there.
make up
#### Running from docker hub
```
docker run --name opnform -v $PWD/my-opnform-data:/persist -p 80:80 jhumanj/opnform
``` ```
The application is now running on [http://localhost:4000](http://localhost:4000). You should now be able to access the application by visiting http://localhost in a web browser.
Alternatively, you may use the compose setup on its own
```bash The `-v` argument creates a local directory called `my-opnform-data` which will store your database and files so that your work is not lost when you restart the container.
# Start the application
docker compose up -d server
# Run php commands, for example The `--name` argument names the running container so that you can refer back to it later, with e.g. `docker stop opnform`. You can use any name you'd like.
docker compose run php-cli artisan about
# ...or update npm dependencies
docker compose run node-cli npm ci #### Using a custom .env file
If you have a custom env file you can use this like so:
```
docker run --name opnform -v $PWD/my-custom-env-file.env:/app/.env -v $PWD/my-opnform-data:/persist -p 80:80 jhumanj/opnform
``` ```
`make` keeps track of all executed targets using `.make.*` files. This would load load in the env file located at `my-custom-env-file.env`, note that if you are creating a .env file for use like this it's best to start from the `.docker.env` example file as there are slightly different defaults for the dockerized setup.
In case you'd like to start from scratch (re-install dependencies, reset jwt
token, run migrations, ...), run
```bash #### Using a custom HTTP port
make clean
To run on port 8080
```
docker run --name opnform -v $PWD/my-opnform-data:/persist -p 8080:80 jhumanj/opnform
``` ```
After that, `make` will re-execute all targets upon the next execution. #### Building a custom docker image
To build a custom docker image from your local source code use this command from the root of the source repository:
```
docker build . -t my-docker-image-name
```
This should create a new docker image tagged `my-docker-image-name` which can be run as follows:
```
docker run --name opnform -v $PWD/my-opnform-data:/persist -p 80:80 my-docker-image-name
```
#### Upgrading docker installations
**Please consult the upgrade instructions for the latest opnform version**, e.g. if upgrading from v1 to v2 please check the v2 instructions as the process may change in future releases.
Normal upgrade procedure would be to stop the running container, back up your data directory (you will need this backup if you want to rollback to the old version) and then start a container running the new image with the same arguments.
e.g. if you're running from a specific opnform version with
```docker run --name opnform -v $PWD/my-opnform-data:/persist -p 80:80 jhumanj/opnform:1.0.0```
You could run:
```
# stop the running container
docker stop opnform
# backup the data directory
cp -r my-opnform-data my-opnform-backup
# start the new container
docker run --name opnform-2 -v $PWD/my-opnform-data:/persist -p 80:80 jhumanj/opnform:2.0.0
```
Then if everything is running smoothly you can delete the old container with:
```
docker rm opnform
```
If you haven't specified a version e.g. if you are using the image `jhumanj/opnform` or `jhumanj/opnform:latest` you will need to run `docker pull jhumanj/opnform` or `docker pull jhumanj/opnform:latest` before starting the new container.
### Using Laravel Valet ### Using Laravel Valet
This section explains how to get started locally with the project. It's most likely relevant if you're trying to work on the project. This section explains how to get started locally with the project. It's most likely relevant if you're trying to work on the project.

View File

@ -1,75 +0,0 @@
---
services:
php-cli:
build:
dockerfile: './php-cli.Dockerfile'
entrypoint: [ 'php' ]
volumes:
- '.:/project'
working_dir: '/project'
user: '1000'
node-cli:
image: 'node:16'
volumes:
- '.:/project'
working_dir: '/project'
user: '1000'
composer-cli:
build:
dockerfile: './php-cli.Dockerfile'
entrypoint: [ 'composer' ]
volumes:
- '.:/project'
working_dir: '/project'
user: '1000'
composer-install:
extends:
service: composer-cli
command: [ 'install' ]
npm-install:
extends:
service: node-cli
entrypoint: [ 'npm' ]
command: [ 'clean-install' ]
npm-build:
extends:
service: node-cli
entrypoint: [ 'npm', 'run' ]
command: [ 'build' ]
database:
image: 'postgres:15'
restart: 'unless-stopped'
env_file: '.env.docker'
healthcheck:
test: [ 'CMD-SHELL', 'pg_isready', '-d', 'postgres' ]
start_period: 5s
interval: 5s
timeout: 10s
retries: 3
migrate:
extends:
service: php-cli
depends_on:
database:
condition: service_healthy
env_file: '.env.docker'
command: [ 'artisan', 'migrate' ]
server:
extends:
service: php-cli
depends_on:
migrate:
condition: service_completed_successfully
env_file: '.env.docker'
command: [ '-S', '0.0.0.0:4000', '-t', './public']
ports:
- '4000:4000'

22
docker/nginx.conf Normal file
View File

@ -0,0 +1,22 @@
server {
listen 80;
server_name opnform;
root /app/public;
access_log /dev/stdout;
error_log /dev/stderr error;
index index.html index.htm index.php;
location / {
try_files $uri $uri/ /index.php$is_args$args;
}
location ~ \.php$ {
fastcgi_split_path_info ^(.+\.php)(/.+)$;
fastcgi_pass unix:/var/run/php-fpm-opnform-site.sock;
fastcgi_index index.php;
include fastcgi.conf;
}
}

35
docker/php-fpm-wrapper.sh Normal file
View File

@ -0,0 +1,35 @@
#!/bin/bash +ex
[ -L /app/storage ] || {
echo "Backing up initial storage directory"
rm -rf /etc/initial-storage
mv /app/storage /etc/initial-storage
}
[ -d /persist/storage ] || {
echo "Initialising blank storage dir"
mkdir -p /persist
cp -a /etc/initial-storage /persist/storage
chmod 777 -R /persist/storage
}
touch /var/log/opnform.log
chown opnform /var/log/opnform.log
echo "Linking persistent storage into app"
ln -sf /persist/storage /app/storage
. /app/.env
[ "x$APP_KEY" != "x" ] || {
artisan key:generate
. /app/.env
}
[ "x$JWT_SECRET" != "x" ] || {
artisan jwt:secret -f
. /app/.env
}
/usr/sbin/php-fpm8.1
tail -f /var/log/opnform.log

18
docker/php-fpm.conf Normal file
View File

@ -0,0 +1,18 @@
[opnform]
user = opnform
group = opnform
listen = /var/run/php-fpm-opnform-site.sock
listen.owner = www-data
listen.group = www-data
php_admin_value[disable_functions] = exec,passthru,shell_exec,system
php_admin_flag[allow_url_fopen] = off
php_admin_value[error_log] = /var/log/opnform.log
; Choose how the process manager will control the number of child processes.
pm = dynamic
pm.max_children = 75
pm.start_servers = 10
pm.min_spare_servers = 5
pm.max_spare_servers = 20
pm.process_idle_timeout = 10s
clear_env = no

View File

@ -0,0 +1,46 @@
#!/bin/bash
DATA_DIR=/persist/pgsql/data
CONFIG_FILE=/etc/postgresql/postgresql.conf
PG_BASE=/usr/lib/postgresql/15/
touch $CONFIG_FILE
mkdir -p $DATA_DIR
chown postgres -R $DATA_DIR
chmod 0700 $DATA_DIR
. /app/.env
test -f $DATA_DIR/postgresql.conf || NEW_DB=true
if [ "x$NEW_DB" != "x" ]; then
echo "No database files found. Initialising blank database"
sudo -u postgres $PG_BASE/bin/initdb -D $DATA_DIR
fi
sudo -u postgres $PG_BASE/bin/postgres -D $DATA_DIR -c config_file=$CONFIG_FILE &
wait_for_database_to_be_ready() {
while ! (echo "select version()" | psql -U $DB_USERNAME); do
echo "Waiting 5 seconds for the database to come up"
sleep 5;
done
}
if [ "x$NEW_DB" != "x" ]; then
echo "Creating database users"
wait_for_database_to_be_ready
psql -U postgres <<EOF
CREATE ROLE $DB_USERNAME LOGIN PASSWORD '$DB_PASSWORD';
CREATE DATABASE $DB_DATABASE;
\c $DB_DATABASE;
GRANT ALL ON DATABASE $DB_DATABASE TO $DB_USERNAME;
GRANT ALL ON SCHEMA public TO $DB_USERNAME;
EOF
fi
wait_for_database_to_be_ready
artisan migrate
wait

7
docker/redis-wrapper.sh Normal file
View File

@ -0,0 +1,7 @@
#!/bin/bash
sysctl vm.overcommit_memory=1
mkdir -p /persist/redis/data
chown redis -R /persist/redis/data
sudo -u redis /usr/bin/redis-server

42
docker/supervisord.conf Normal file
View File

@ -0,0 +1,42 @@
[supervisord]
nodaemon=true
logfile=/dev/null
logfile_maxbytes=0
[program:nginx]
command=/usr/sbin/nginx
stdout_logfile=/dev/stdout
stderr_logfile=/dev/stderr
stdout_logfile_maxbytes=0
redirect_stderr=true
[program:php-fpm]
command=/usr/local/bin/php-fpm-wrapper.sh
stdout_logfile=/dev/stdout
stderr_logfile=/dev/stderr
stdout_logfile_maxbytes=0
redirect_stderr=true
[program:php-queue]
process_name=%(program_name)s_%(process_num)02d
command=/usr/local/bin/artisan queue:work
stdout_logfile=/dev/stdout
stderr_logfile=/dev/stderr
stdout_logfile_maxbytes=0
redirect_stderr=true
numprocs=5
[program:postgres]
command=/usr/local/bin/postgres-wrapper.sh
stdout_logfile=/dev/stdout
stderr_logfile=/dev/stderr
stdout_logfile_maxbytes=0
redirect_stderr=true
[program:redis]
command=/usr/local/bin/redis-wrapper.sh
stdout_logfile=/dev/stdout
stderr_logfile=/dev/stderr
stdout_logfile_maxbytes=0
redirect_stderr=true

View File

@ -1,30 +0,0 @@
# syntax=docker/dockerfile:1.3-labs
FROM php:8.1-cli
COPY --from=mlocati/php-extension-installer /usr/bin/install-php-extensions /usr/bin/
COPY --from=composer:2 /usr/bin/composer /usr/bin/composer
RUN <<EOF
install-php-extensions \
apcu \
bcmath \
bz2 \
calendar \
ffi \
gd \
gmp \
imagick \
intl \
mysqli \
pcntl \
pcov \
pdo_mysql \
pdo_pgsql \
redis \
soap \
sockets \
sodium \
xsl \
zip \
exif
EOF