From 8f84faf3d1577abf2d944e434d4b6a57f2216d0c Mon Sep 17 00:00:00 2001 From: Don Benjamin Date: Thu, 27 Jul 2023 10:34:19 +0100 Subject: [PATCH] =?UTF-8?q?(#146)=20Re-wrote=20the=20docker=20code=20to=20?= =?UTF-8?q?generate=20a=20single=20self-contained=20d=E2=80=A6=20(#153)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * (#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. --- .dockerignore | 2 + .env.docker | 67 ++++++++++++++++++++++--- .github/workflows/dockerhub.yml | 26 ++++++++++ Dockerfile | 87 +++++++++++++++++++++++++++++++++ Makefile | 43 ---------------- README.md | 85 +++++++++++++++++++++++++------- docker-compose.yml | 75 ---------------------------- docker/nginx.conf | 22 +++++++++ docker/php-fpm-wrapper.sh | 35 +++++++++++++ docker/php-fpm.conf | 18 +++++++ docker/postgres-wrapper.sh | 46 +++++++++++++++++ docker/redis-wrapper.sh | 7 +++ docker/supervisord.conf | 42 ++++++++++++++++ php-cli.Dockerfile | 30 ------------ 14 files changed, 412 insertions(+), 173 deletions(-) create mode 100644 .dockerignore create mode 100644 .github/workflows/dockerhub.yml create mode 100644 Dockerfile delete mode 100644 Makefile delete mode 100644 docker-compose.yml create mode 100644 docker/nginx.conf create mode 100644 docker/php-fpm-wrapper.sh create mode 100644 docker/php-fpm.conf create mode 100644 docker/postgres-wrapper.sh create mode 100644 docker/redis-wrapper.sh create mode 100644 docker/supervisord.conf delete mode 100644 php-cli.Dockerfile diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..331d387 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,2 @@ +.git +Dockerfile diff --git a/.env.docker b/.env.docker index 185843d..b907e68 100644 --- a/.env.docker +++ b/.env.docker @@ -1,9 +1,62 @@ -POSTGRES_USER="postgres" -POSTGRES_PASSWORD="postgres" -POSTGRES_DB="postgres" +APP_NAME="OpnForm" +APP_ENV=local +APP_KEY= +APP_DEBUG=false +APP_LOG_LEVEL=debug +APP_URL=http://localhost -DB_HOST="database" -DB_DATABASE="postgres" -DB_USERNAME="postgres" -DB_PASSWORD="postgres" +LOG_CHANNEL=errorlog +LOG_LEVEL=debug + +DB_CONNECTION=pgsql +DB_HOST=127.0.0.1 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= diff --git a/.github/workflows/dockerhub.yml b/.github/workflows/dockerhub.yml new file mode 100644 index 0000000..9bf6e3f --- /dev/null +++ b/.github/workflows/dockerhub.yml @@ -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 diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..f11c606 --- /dev/null +++ b/Dockerfile @@ -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 diff --git a/Makefile b/Makefile deleted file mode 100644 index 89ac1f5..0000000 --- a/Makefile +++ /dev/null @@ -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 diff --git a/README.md b/README.md index dbf3b0f..cf0cec6 100644 --- a/README.md +++ b/README.md @@ -44,35 +44,84 @@ It takes 1 minute to try out the builder for free. You'll have high availability ### 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 -make up +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. + +#### 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). -Alternatively, you may use the compose setup on its own +You should now be able to access the application by visiting http://localhost in a web browser. -```bash -# Start the application -docker compose up -d server +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. -# Run php commands, for example -docker compose run php-cli artisan about +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. -# ...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. -In case you'd like to start from scratch (re-install dependencies, reset jwt -token, run migrations, ...), run +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. -```bash -make clean +#### Using a custom HTTP port + +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 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. diff --git a/docker-compose.yml b/docker-compose.yml deleted file mode 100644 index ca5e09e..0000000 --- a/docker-compose.yml +++ /dev/null @@ -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' diff --git a/docker/nginx.conf b/docker/nginx.conf new file mode 100644 index 0000000..b8fc41b --- /dev/null +++ b/docker/nginx.conf @@ -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; + } +} + diff --git a/docker/php-fpm-wrapper.sh b/docker/php-fpm-wrapper.sh new file mode 100644 index 0000000..8cf5e0e --- /dev/null +++ b/docker/php-fpm-wrapper.sh @@ -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 diff --git a/docker/php-fpm.conf b/docker/php-fpm.conf new file mode 100644 index 0000000..c40aa18 --- /dev/null +++ b/docker/php-fpm.conf @@ -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 + diff --git a/docker/postgres-wrapper.sh b/docker/postgres-wrapper.sh new file mode 100644 index 0000000..80f4769 --- /dev/null +++ b/docker/postgres-wrapper.sh @@ -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 <