├── .coderabbit.yaml ├── .env ├── .github ├── CODEOWNERS ├── FUNDING.yml ├── dependabot.yml └── workflows │ ├── ghostv5.yml │ └── linters │ ├── .dockerfilelintrc │ └── .hadolint.yaml ├── .gitignore ├── .stickler.yml ├── LICENSE ├── README.md └── v5 ├── .dockerignore ├── Dockerfile ├── config.production.json ├── docker-entrypoint.sh ├── test-build.sh └── test └── config.sh /.coderabbit.yaml: -------------------------------------------------------------------------------- 1 | language: en-CA 2 | tone_instructions: "" 3 | early_access: false 4 | enable_free_tier: true 5 | reviews: 6 | profile: assertive 7 | request_changes_workflow: false 8 | high_level_summary: true 9 | high_level_summary_placeholder: "@coderabbitai summary" 10 | high_level_summary_in_walkthrough: false 11 | auto_title_placeholder: "@coderabbitai" 12 | auto_title_instructions: "" 13 | review_status: true 14 | commit_status: true 15 | fail_commit_status: false 16 | collapse_walkthrough: false 17 | changed_files_summary: true 18 | sequence_diagrams: true 19 | assess_linked_issues: true 20 | related_issues: true 21 | related_prs: true 22 | suggested_labels: true 23 | auto_apply_labels: true 24 | suggested_reviewers: true 25 | auto_assign_reviewers: false 26 | poem: false 27 | labeling_instructions: [] 28 | path_filters: [] 29 | path_instructions: [] 30 | abort_on_close: true 31 | disable_cache: false 32 | auto_review: 33 | enabled: true 34 | auto_incremental_review: true 35 | ignore_title_keywords: [] 36 | labels: [] 37 | drafts: false 38 | base_branches: [] 39 | finishing_touches: 40 | docstrings: 41 | enabled: true 42 | unit_tests: 43 | enabled: true 44 | tools: 45 | ast-grep: 46 | rule_dirs: [] 47 | util_dirs: [] 48 | essential_rules: true 49 | packages: [] 50 | shellcheck: 51 | enabled: true 52 | ruff: 53 | enabled: true 54 | markdownlint: 55 | enabled: true 56 | github-checks: 57 | enabled: true 58 | timeout_ms: 90000 59 | languagetool: 60 | enabled: true 61 | enabled_rules: [] 62 | disabled_rules: [] 63 | enabled_categories: [] 64 | disabled_categories: [] 65 | enabled_only: false 66 | level: default 67 | biome: 68 | enabled: true 69 | hadolint: 70 | enabled: true 71 | swiftlint: 72 | enabled: true 73 | phpstan: 74 | enabled: true 75 | level: default 76 | golangci-lint: 77 | enabled: true 78 | yamllint: 79 | enabled: true 80 | gitleaks: 81 | enabled: true 82 | checkov: 83 | enabled: true 84 | detekt: 85 | enabled: true 86 | eslint: 87 | enabled: true 88 | rubocop: 89 | enabled: true 90 | buf: 91 | enabled: true 92 | regal: 93 | enabled: true 94 | actionlint: 95 | enabled: true 96 | pmd: 97 | enabled: true 98 | cppcheck: 99 | enabled: true 100 | semgrep: 101 | enabled: true 102 | circleci: 103 | enabled: true 104 | clippy: 105 | enabled: true 106 | sqlfluff: 107 | enabled: true 108 | prismaLint: 109 | enabled: true 110 | pylint: 111 | enabled: true 112 | oxc: 113 | enabled: true 114 | shopifyThemeCheck: 115 | enabled: true 116 | luacheck: 117 | enabled: true 118 | brakeman: 119 | enabled: true 120 | dotenvLint: 121 | enabled: true 122 | chat: 123 | auto_reply: true 124 | integrations: 125 | jira: 126 | usage: auto 127 | linear: 128 | usage: auto 129 | knowledge_base: 130 | opt_out: false 131 | web_search: 132 | enabled: true 133 | learnings: 134 | scope: auto 135 | issues: 136 | scope: auto 137 | jira: 138 | usage: auto 139 | project_keys: [] 140 | linear: 141 | usage: auto 142 | team_keys: [] 143 | pull_requests: 144 | scope: auto 145 | code_generation: 146 | docstrings: 147 | language: en-CA 148 | path_instructions: [] 149 | unit_tests: 150 | path_instructions: [] 151 | -------------------------------------------------------------------------------- /.env: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | ### Overriding defaults var in `.bl_override.sh` 3 | 4 | APP_NAME="ghostfire" 5 | GITHUB_USER="firepress-org" 6 | APP_VERSION="5.112.0" 7 | 8 | ### function options 9 | CFG_USE_GPG_SIGNATURE="false" 10 | 11 | ### Needed when projects are under sub directories 12 | CFG_SUB_DIR="v5" 13 | 14 | ### branch names 15 | CFG_DEFAULT_BRANCH="master" 16 | -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @pascalandy -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: firepress-org 2 | custom: https://firepress.org/en/pricing/ -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | # Maintain dependencies for GitHub Actions 4 | - package-ecosystem: "github-actions" 5 | directory: "/" 6 | schedule: 7 | interval: "weekly" 8 | time: "07:00" 9 | timezone: "America/New_York" 10 | labels: 11 | - ":game_die: dependencies" 12 | - ":robot: bot" 13 | 14 | # Maintain dependencies for Docker 15 | - package-ecosystem: "docker" 16 | directory: "/" 17 | schedule: 18 | interval: "weekly" 19 | time: "07:00" 20 | timezone: "America/New_York" 21 | labels: 22 | - ":game_die: dependencies" 23 | - ":robot: bot" 24 | registries: 25 | - dockerhub # This links the Docker ecosystem to the dockerhub registry 26 | 27 | registries: 28 | dockerhub: 29 | type: docker-registry 30 | url: https://registry.hub.docker.com 31 | username: ${{ needs.myvars.outputs.DOCKERHUB_USER }} 32 | password: ${{ secrets.DOCKERHUB_PASSWORD }} 33 | -------------------------------------------------------------------------------- /.github/workflows/ghostv5.yml: -------------------------------------------------------------------------------- 1 | # The beauty of our CI setup is that it will build Dockerfile and set variables from the Dockerfile 2 | # by https://pascalandy.com at https://firepress.org 3 | # GNU v3 https://github.com/firepress-org/ghostfire/blob/master/LICENSE 4 | 5 | name: Ghost V5 alpine 6 | 7 | on: 8 | schedule: 9 | - cron: "0 5 * * *" # everyday at 7am 10 | pull_request: 11 | branches: [master] 12 | paths: 13 | - "v5/**" 14 | - ".github/workflows/**" 15 | push: 16 | tags: 17 | - "5.*.*" 18 | paths-ignore: 19 | - "**.md" 20 | # comment this out means : a build happens on every branch 21 | # branches: [master] 22 | 23 | # Allows you to run this workflow manually from the Actions tab 24 | workflow_dispatch: 25 | 26 | env: 27 | # needed to define path within 'uses' 28 | SUB_DIR: "v5" 29 | 30 | permissions: 31 | contents: read 32 | packages: write # if you're pushing to GitHub Packages 33 | pull-requests: write # allows Dependabot to auto-merge its PRs if you've configured it to do so 34 | 35 | defaults: 36 | run: 37 | shell: "bash -Eeuo pipefail -x {0}" 38 | # works within 'run' but not with 'uses' 39 | working-directory: "v5" 40 | 41 | jobs: 42 | # ---------------------------------------------- 43 | # SET VARIABLES 44 | # More context about the way we set variables https://github.com/firepress-org/ghostfire/issues/46 45 | # ---------------------------------------------- 46 | myvars: 47 | runs-on: ubuntu-latest 48 | steps: 49 | - uses: actions/checkout@v4 50 | with: 51 | fetch-depth: "0" 52 | - uses: zcong1993/setup-timezone@v2.0.0 53 | with: 54 | timezone: America/Montreal 55 | - name: Github Actions system status 56 | uses: crazy-max/ghaction-github-status@v4.2.0 57 | with: 58 | actions_threshold: operational 59 | git_threshold: degraded_performance 60 | 61 | - name: Create dir for our varz 62 | run: mkdir -pv ~/varz 63 | # Build Logic 64 | # By default, every builds uses 'edge' workflow 65 | # Commits from a PR, it uses 'edge' workflow 66 | # When a PR is merge/rebase into master, it uses 'edge' workflow 67 | # When a schedule build rund, it uses 'edge' workflow 68 | # Commits on master/main uses, it uses 'edge' workflow (I do this all the time to test ci yaml) 69 | # Once everything is clean .. 70 | # I TAG a commit (i.e. 5.19.1), then the CI uses 'stable' workflow and push the OFFICIAL docker image 71 | - name: edge | Set BRANCH_NAME 72 | if: github.ref_type != 'tag' 73 | run: echo "edge" > ~/varz/BRANCH_NAME 74 | - name: stable | Set BRANCH_NAME 75 | if: github.ref_type == 'tag' 76 | run: echo "stable" > ~/varz/BRANCH_NAME 77 | 78 | - name: Define and save variables to disk 79 | run: | 80 | cat Dockerfile | grep APP_NAME= | head -n 1 | grep -o '".*"' | sed 's/"//g' > ~/varz/APP_NAME 81 | cat Dockerfile | grep VERSION= | head -n 1 | grep -o '".*"' | sed 's/"//g' > ~/varz/VERSION_TMP 82 | echo "— — — DO NOT MIX alpine vs debian Tags! — — — " 83 | echo "$(cat ~/varz/VERSION_TMP)" > ~/varz/VERSION 84 | cat Dockerfile | grep DOCKERHUB_USER= | head -n 1 | grep -o '".*"' | sed 's/"//g' > ~/varz/DOCKERHUB_USER 85 | cat Dockerfile | grep GITHUB_ORG= | head -n 1 | grep -o '".*"' | sed 's/"//g' > ~/varz/GITHUB_ORG 86 | cat Dockerfile | grep GITHUB_REGISTRY= | head -n 1 | grep -o '".*"' | sed 's/"//g' > ~/varz/GITHUB_REGISTRY 87 | echo "$(cat ~/varz/DOCKERHUB_USER)/$(cat ~/varz/APP_NAME)" > ~/varz/DKR_PREFIX 88 | echo "ghcr.io/$(cat ~/varz/GITHUB_ORG)/$(cat ~/varz/GITHUB_REGISTRY)/$(cat ~/varz/APP_NAME)" > ~/varz/GPR_PREFIX 89 | git rev-parse --short HEAD > ~/varz/SHORT_HASH_COMMIT 90 | date "+%Y-%m-%d_%HH%M" > ~/varz/DATE 91 | echo "$(cat ~/varz/VERSION)_$(cat ~/varz/SHORT_HASH_COMMIT)" > ~/varz/VERSION_HASH_ONLY 92 | echo "$(cat ~/varz/VERSION)_$(cat ~/varz/SHORT_HASH_COMMIT)_$(cat ~/varz/DATE)" > ~/varz/VERSION_HASH_DATE 93 | echo "$(cat ~/varz/APP_NAME):$(cat ~/varz/VERSION)_$(cat ~/varz/SHORT_HASH_COMMIT)" > ~/varz/_NOTI_MESSAGE 94 | echo "$(cat ~/varz/DKR_PREFIX):$(cat ~/varz/VERSION)" > ~/varz/TAG_DKR_VERSION 95 | echo "$(cat ~/varz/DKR_PREFIX):$(cat ~/varz/BRANCH_NAME)" > ~/varz/TAG_DKR_BRANCH_NAME 96 | echo "$(cat ~/varz/DKR_PREFIX):$(cat ~/varz/VERSION_HASH_ONLY)" > ~/varz/TAG_DKR_VERSION_HASH_ONLY 97 | echo "$(cat ~/varz/DKR_PREFIX):$(cat ~/varz/VERSION_HASH_DATE)" > ~/varz/TAG_DKR_VERSION_HASH_DATE 98 | echo "$(cat ~/varz/GPR_PREFIX):$(cat ~/varz/VERSION)" > ~/varz/TAG_GPR_VERSION 99 | echo "$(cat ~/varz/GPR_PREFIX):$(cat ~/varz/BRANCH_NAME)" > ~/varz/TAG_GPR_BRANCH_NAME 100 | echo "$(cat ~/varz/GPR_PREFIX):$(cat ~/varz/VERSION_HASH_ONLY)" > ~/varz/TAG_GPR_VERSION_HASH_ONLY 101 | echo "$(cat ~/varz/GPR_PREFIX):$(cat ~/varz/VERSION_HASH_DATE)" > ~/varz/TAG_GPR_VERSION_HASH_DATE 102 | - name: Upload variables as artifact 103 | uses: actions/upload-artifact@master 104 | with: 105 | name: variables_on_disk 106 | path: ~/varz 107 | - name: Expose variables to all jobs 108 | id: myvars 109 | run: | 110 | echo "APP_NAME=$(cat ~/varz/APP_NAME)" >> "$GITHUB_OUTPUT" 111 | echo "VERSION=$(cat ~/varz/VERSION)" >> "$GITHUB_OUTPUT" 112 | echo "DOCKERHUB_USER=$(cat ~/varz/DOCKERHUB_USER)" >> "$GITHUB_OUTPUT" 113 | echo "GITHUB_ORG=$(cat ~/varz/GITHUB_ORG)" >> "$GITHUB_OUTPUT" 114 | echo "GITHUB_REGISTRY=$(cat ~/varz/GITHUB_REGISTRY)" >> "$GITHUB_OUTPUT" 115 | echo "DKR_PREFIX=$(cat ~/varz/DKR_PREFIX)" >> "$GITHUB_OUTPUT" 116 | echo "GPR_PREFIX=$(cat ~/varz/GPR_PREFIX)" >> "$GITHUB_OUTPUT" 117 | echo "SUB_DIR=$(cat ~/varz/SUB_DIR)" >> "$GITHUB_OUTPUT" 118 | echo "SHORT_HASH_COMMIT=$(cat ~/varz/SHORT_HASH_COMMIT)" >> "$GITHUB_OUTPUT" 119 | echo "DATE=$(cat ~/varz/DATE)" >> "$GITHUB_OUTPUT" 120 | echo "BRANCH_NAME=$(cat ~/varz/BRANCH_NAME)" >> "$GITHUB_OUTPUT" 121 | echo "VERSION_HASH_ONLY=$(cat ~/varz/VERSION_HASH_ONLY)" >> "$GITHUB_OUTPUT" 122 | echo "VERSION_HASH_DATE=$(cat ~/varz/VERSION_HASH_DATE)" >> "$GITHUB_OUTPUT" 123 | echo "_NOTI_MESSAGE=$(cat ~/varz/_NOTI_MESSAGE)" >> "$GITHUB_OUTPUT" 124 | echo "TAG_DKR_VERSION=$(cat ~/varz/TAG_DKR_VERSION)" >> "$GITHUB_OUTPUT" 125 | echo "TAG_DKR_BRANCH_NAME=$(cat ~/varz/TAG_DKR_BRANCH_NAME)" >> "$GITHUB_OUTPUT" 126 | echo "TAG_DKR_VERSION_HASH_ONLY=$(cat ~/varz/TAG_DKR_VERSION_HASH_ONLY)" >> "$GITHUB_OUTPUT" 127 | echo "TAG_DKR_VERSION_HASH_DATE=$(cat ~/varz/TAG_DKR_VERSION_HASH_DATE)" >> "$GITHUB_OUTPUT" 128 | echo "TAG_GPR_VERSION=$(cat ~/varz/TAG_GPR_VERSION)" >> "$GITHUB_OUTPUT" 129 | echo "TAG_GPR_BRANCH_NAME=$(cat ~/varz/TAG_GPR_BRANCH_NAME)" >> "$GITHUB_OUTPUT" 130 | echo "TAG_GPR_VERSION_HASH_ONLY=$(cat ~/varz/TAG_GPR_VERSION_HASH_ONLY)" >> "$GITHUB_OUTPUT" 131 | echo "TAG_GPR_VERSION_HASH_DATE=$(cat ~/varz/TAG_GPR_VERSION_HASH_DATE)" >> "$GITHUB_OUTPUT" 132 | outputs: 133 | APP_NAME: ${{ steps.myvars.outputs.APP_NAME }} 134 | VERSION: ${{ steps.myvars.outputs.VERSION }} 135 | SUB_DIR: ${{ steps.myvars.outputs.SUB_DIR }} 136 | DOCKERHUB_USER: ${{ steps.myvars.outputs.DOCKERHUB_USER }} 137 | GITHUB_ORG: ${{ steps.myvars.outputs.GITHUB_ORG }} 138 | GITHUB_REGISTRY: ${{ steps.myvars.outputs.GITHUB_REGISTRY }} 139 | DKR_PREFIX: ${{ steps.myvars.outputs.DKR_PREFIX }} 140 | GPR_PREFIX: ${{ steps.myvars.outputs.GPR_PREFIX }} 141 | SHORT_HASH_COMMIT: ${{ steps.myvars.outputs.SHORT_HASH_COMMIT }} 142 | DATE: ${{ steps.myvars.outputs.DATE }} 143 | BRANCH_NAME: ${{ steps.myvars.outputs.BRANCH_NAME }} 144 | VERSION_HASH_ONLY: ${{ steps.myvars.outputs.VERSION_HASH_ONLY }} 145 | VERSION_HASH_DATE: ${{ steps.myvars.outputs.VERSION_HASH_DATE }} 146 | _NOTI_MESSAGE: ${{ steps.myvars.outputs._NOTI_MESSAGE }} 147 | TAG_DKR_VERSION: ${{ steps.myvars.outputs.TAG_DKR_VERSION }} 148 | TAG_DKR_BRANCH_NAME: ${{ steps.myvars.outputs.TAG_DKR_BRANCH_NAME }} 149 | TAG_DKR_VERSION_HASH_ONLY: ${{ steps.myvars.outputs.TAG_DKR_VERSION_HASH_ONLY }} 150 | TAG_DKR_VERSION_HASH_DATE: ${{ steps.myvars.outputs.TAG_DKR_VERSION_HASH_DATE }} 151 | TAG_GPR_VERSION: ${{ steps.myvars.outputs.TAG_GPR_VERSION }} 152 | TAG_GPR_BRANCH_NAME: ${{ steps.myvars.outputs.TAG_GPR_BRANCH_NAME }} 153 | TAG_GPR_VERSION_HASH_ONLY: ${{ steps.myvars.outputs.TAG_GPR_VERSION_HASH_ONLY }} 154 | TAG_GPR_VERSION_HASH_DATE: ${{ steps.myvars.outputs.TAG_GPR_VERSION_HASH_DATE }} 155 | 156 | # ---------------------------------------------- 157 | # build / edge OR stable 158 | # tags are not the same between edge or stable 159 | # ---------------------------------------------- 160 | build_edge: 161 | needs: [myvars] 162 | if: github.ref_type != 'tag' 163 | runs-on: ubuntu-latest 164 | steps: 165 | - uses: actions/checkout@v4 166 | with: 167 | fetch-depth: 0 168 | - uses: zcong1993/setup-timezone@v2.0.0 169 | with: 170 | timezone: America/Montreal 171 | - uses: docker/setup-qemu-action@v3 172 | - uses: docker/setup-buildx-action@v3 173 | with: 174 | version: latest 175 | - uses: docker/login-action@v3.4.0 176 | with: 177 | username: ${{ needs.myvars.outputs.DOCKERHUB_USER }} 178 | password: ${{ github.actor == 'dependabot[bot]' && secrets.DEPENDABOT_DOCKERHUB_PASSWORD || secrets.DOCKERHUB_PASSWORD }} 179 | - uses: docker/login-action@v3.4.0 180 | with: 181 | registry: ghcr.io 182 | username: ${{ needs.myvars.outputs.GITHUB_ORG }} 183 | password: ${{ github.actor == 'dependabot[bot]' && secrets.TOKEN_GPR || secrets.TOKEN_GPR }} 184 | 185 | - name: Build and push image (edge) 186 | uses: docker/build-push-action@v6 187 | with: 188 | context: . 189 | file: ./${{ env.SUB_DIR }}/Dockerfile 190 | platforms: | 191 | linux/amd64 192 | # linux/arm64/v8 193 | # linux/arm/v8 194 | # linux/arm/v7 195 | push: true 196 | tags: | 197 | ${{ needs.myvars.outputs.TAG_DKR_BRANCH_NAME }} 198 | ${{ needs.myvars.outputs.TAG_GPR_BRANCH_NAME }} 199 | # The order for matters for our CD down the line 200 | cache-from: type=gha, scope=${{ github.workflow }} 201 | cache-to: type=gha, scope=${{ github.workflow }}, mode=max 202 | build-args: | 203 | BUILDKIT_PROGRESS=plain 204 | network: host 205 | 206 | - name: UAT run edge | Wait for container to run 207 | timeout-minutes: 2 208 | # These two VAR are here to force SQLite over Mysql 209 | # database__client=s 210 | # database__connection__filename= 211 | run: | 212 | docker run -d --name ghostUAT -p 2368:2368 -e WEB_URL=http://localhost:2368 -e NODE_ENV=production -e database__client=sqlite3 -e database__connection__filename=/var/lib/ghost/content/data/ghost.db ${{ needs.myvars.outputs.TAG_DKR_BRANCH_NAME }} 213 | echo "--- Wait for ghostUAT --->" 214 | until $(curl --output /dev/null --silent --head --fail http://localhost:2368); do 215 | echo "--- ghostUAT is starting..." 216 | sleep 1 217 | done; 218 | echo "--- ghostUAT is running! --->" 219 | 220 | - name: UAT docker test edge | docker-library tests 221 | # Ghost V5 grep "Thoughts, stories and ideas" 222 | # Ghost V4 grep "Thoughts, stories and ideas" 223 | # Ghost V3 grep "The professional publishing platform" 224 | run: | 225 | curl http://localhost:2368 | grep "Thoughts, stories and ideas" 226 | git clone --depth 1 https://github.com/docker-library/official-images.git official-images 227 | cp ./test/config.sh official-images/test/config.sh 228 | official-images/test/run.sh ${{ needs.myvars.outputs.TAG_DKR_BRANCH_NAME }} 229 | 230 | - name: Checkpoint edge | image ls 231 | run: docker image ls 232 | 233 | - name: Docker Scout edge 234 | id: docker-scout-edge 235 | uses: docker/scout-action@v1 236 | with: 237 | command: compare 238 | image: node:20.19.2-alpine3.22 239 | to: ${{ needs.myvars.outputs.TAG_DKR_BRANCH_NAME }} 240 | ignore-unchanged: true 241 | only-severities: critical,high,medium,low 242 | write-comment: true 243 | github-token: ${{ secrets.GITHUB_TOKEN }} # to be able to write the comment 244 | 245 | build_stable: 246 | needs: [myvars] 247 | if: github.ref_type == 'tag' 248 | runs-on: ubuntu-latest 249 | steps: 250 | - uses: actions/checkout@v4 251 | with: 252 | fetch-depth: 0 253 | - uses: zcong1993/setup-timezone@v2.0.0 254 | with: 255 | timezone: America/Montreal 256 | - uses: docker/setup-qemu-action@v3 257 | - uses: docker/setup-buildx-action@v3 258 | with: 259 | version: latest 260 | - uses: docker/login-action@v3.4.0 261 | with: 262 | username: ${{ needs.myvars.outputs.DOCKERHUB_USER }} 263 | password: ${{ github.actor == 'dependabot[bot]' && secrets.DEPENDABOT_DOCKERHUB_PASSWORD || secrets.DOCKERHUB_PASSWORD }} 264 | - uses: docker/login-action@v3.4.0 265 | with: 266 | registry: ghcr.io 267 | username: ${{ needs.myvars.outputs.GITHUB_ORG }} 268 | password: ${{ github.actor == 'dependabot[bot]' && secrets.TOKEN_GPR || secrets.TOKEN_GPR }} 269 | 270 | - name: Build and push image (stable) 271 | uses: docker/build-push-action@v6 272 | with: 273 | context: . 274 | file: ./${{ env.SUB_DIR }}/Dockerfile 275 | platforms: | 276 | linux/amd64 277 | # linux/arm64/v8 278 | # linux/arm/v8 279 | # linux/arm/v7 280 | push: true 281 | tags: | 282 | ${{ needs.myvars.outputs.TAG_DKR_VERSION }} 283 | ${{ needs.myvars.outputs.TAG_DKR_BRANCH_NAME }} 284 | ${{ needs.myvars.outputs.TAG_DKR_VERSION_HASH_ONLY }} 285 | ${{ needs.myvars.outputs.TAG_DKR_VERSION_HASH_DATE }} 286 | ${{ needs.myvars.outputs.TAG_GPR_VERSION }} 287 | ${{ needs.myvars.outputs.TAG_GPR_BRANCH_NAME }} 288 | ${{ needs.myvars.outputs.TAG_GPR_VERSION_HASH_ONLY }} 289 | ${{ needs.myvars.outputs.TAG_GPR_VERSION_HASH_DATE }} 290 | # The order for matters for our CD down the line 291 | cache-from: type=gha, scope=${{ github.workflow }} 292 | cache-to: type=gha, scope=${{ github.workflow }}, mode=max 293 | build-args: | 294 | BUILDKIT_PROGRESS=plain 295 | network: host 296 | 297 | - name: UAT run stable | Wait for container to run 298 | timeout-minutes: 2 299 | # These two VAR are here to force SQLite over Mysql 300 | # database__client=s 301 | # database__connection__filename= 302 | run: | 303 | docker run -d --name ghostUAT -p 2368:2368 -e WEB_URL=http://localhost:2368 -e NODE_ENV=production -e database__client=sqlite3 -e database__connection__filename=/var/lib/ghost/content/data/ghost.db ${{ needs.myvars.outputs.TAG_DKR_VERSION_HASH_DATE }} 304 | echo "--- Wait for ghostUAT --->" 305 | until $(curl --output /dev/null --silent --head --fail http://localhost:2368); do 306 | echo "--- ghostUAT is starting..." 307 | sleep 1 308 | done; 309 | echo "--- ghostUAT is running! --->" 310 | 311 | - name: UAT docker test stable | docker-library tests 312 | # Ghost V5 grep "0o0o" 313 | # Ghost V4 grep "Thoughts, stories and ideas" 314 | # Ghost V3 grep "The professional publishing platform" 315 | run: | 316 | curl http://localhost:2368 | grep "Thoughts, stories and ideas" 317 | git clone --depth 1 https://github.com/docker-library/official-images.git official-images 318 | cp ./test/config.sh official-images/test/config.sh 319 | official-images/test/run.sh ${{ needs.myvars.outputs.TAG_DKR_VERSION_HASH_DATE }} 320 | 321 | - name: Checkpoint | image ls 322 | run: docker image ls 323 | 324 | # ---------------------------------------------- 325 | # UAT / edge OR stable 326 | # This job is bypassed: weird bug on the uat job. Ci complain about docker login. So the UAT is under the build job. 327 | # ---------------------------------------------- 328 | 329 | # ---------------------------------------------- 330 | # Continuous Deployment / edge OR stable 331 | # I duplicate this in two jobs to have a better visual representation 332 | # ---------------------------------------------- 333 | # cd_edge: 334 | # see unused_workflow/cd_edge.yml 335 | 336 | cd_stable: 337 | needs: [build_stable] 338 | if: github.ref_type == 'tag' 339 | runs-on: ubuntu-latest 340 | steps: 341 | - uses: actions/checkout@v4 342 | with: 343 | fetch-depth: 0 344 | - uses: zcong1993/setup-timezone@v2.0.0 345 | with: 346 | timezone: America/Montreal 347 | 348 | - name: Add a comment (from a tag) 349 | run: | 350 | echo "## For reference (from a tag)" >> $GITHUB_STEP_SUMMARY 351 | echo "Docker image: ${{ needs.myvars.outputs.TAG_DKR_VERSION_HASH_DATE }}" >> $GITHUB_STEP_SUMMARY 352 | echo "Please use docker images that were build from a tag. More details: https://github.com/firepress-org/ghostfire/issues/265" >> $GITHUB_STEP_SUMMARY 353 | 354 | # ---------------------------------------------- 355 | # Reviews 356 | # ---------------------------------------------- 357 | review_myvars: 358 | needs: [myvars] 359 | runs-on: ubuntu-latest 360 | steps: 361 | - uses: actions/checkout@v4 362 | with: 363 | fetch-depth: 0 364 | - uses: zcong1993/setup-timezone@v2.0.0 365 | with: 366 | timezone: America/Montreal 367 | - name: Show variables 368 | run: | 369 | echo "Final Docker image name:" 370 | echo "= = = = = = = = = =" 371 | echo ${{ needs.myvars.outputs.TAG_DKR_VERSION }} 372 | echo ${{ needs.myvars.outputs.TAG_DKR_BRANCH_NAME }} 373 | echo ${{ needs.myvars.outputs.TAG_DKR_VERSION_HASH_ONLY }} 374 | echo ${{ needs.myvars.outputs.TAG_DKR_VERSION_HASH_DATE }} 375 | echo ${{ needs.myvars.outputs.TAG_GPR_VERSION }} 376 | echo ${{ needs.myvars.outputs.TAG_GPR_BRANCH_NAME }} 377 | echo ${{ needs.myvars.outputs.TAG_GPR_VERSION_HASH_ONLY }} 378 | echo ${{ needs.myvars.outputs.TAG_GPR_VERSION_HASH_DATE }} 379 | echo "---" 380 | echo "Core environment variables:" 381 | echo "---" 382 | echo "${{ env.SUB_DIR }} << SUB_DIR" 383 | echo "${{ needs.myvars.outputs.APP_NAME }} << APP_NAME" 384 | echo "${{ needs.myvars.outputs.DOCKERHUB_USER }} << DOCKERHUB_USER" 385 | echo "${{ needs.myvars.outputs.GITHUB_ORG }} << GITHUB_ORG" 386 | echo "${{ needs.myvars.outputs.GITHUB_REGISTRY }} << GITHUB_REGISTRY" 387 | echo "${{ needs.myvars.outputs.DKR_PREFIX }} << DKR_PREFIX" 388 | echo "${{ needs.myvars.outputs.GPR_PREFIX }} << GPR_PREFIX" 389 | echo "${{ needs.myvars.outputs.VERSION }} << VERSION" 390 | echo "${{ needs.myvars.outputs.SHORT_HASH_COMMIT }} << SHORT_HASH_COMMIT" 391 | echo "${{ needs.myvars.outputs.DATE }} << DATE" 392 | echo "${{ needs.myvars.outputs.BRANCH_NAME }} << BRANCH_NAME" 393 | echo "${{ needs.myvars.outputs.VERSION_HASH_ONLY }} << VERSION_HASH_ONLY" 394 | echo "${{ needs.myvars.outputs.VERSION_HASH_DATE }} << VERSION_HASH_DATE" 395 | echo "${{ needs.myvars.outputs._NOTI_MESSAGE }} << _NOTI_MESSAGE" 396 | echo "---" 397 | echo "Environment variables provided Github Actions :" 398 | echo "---" 399 | echo "${GITHUB_WORKFLOW} << GITHUB_WORKFLOW" 400 | echo "${GITHUB_RUN_ID} << GITHUB_RUN_ID" 401 | echo "${GITHUB_RUN_NUMBER} << GITHUB_RUN_NUMBER" 402 | echo "${GITHUB_JOB} << GITHUB_JOB" 403 | echo "${GITHUB_ACTION} << GITHUB_ACTION" 404 | echo "${GITHUB_ACTOR} << GITHUB_ACTOR" 405 | echo "${GITHUB_REPOSITORY} << GITHUB_REPOSITORY" 406 | echo "${GITHUB_EVENT_NAME} << GITHUB_EVENT_NAME" 407 | echo "${GITHUB_EVENT_PATH} << GITHUB_EVENT_PATH" 408 | echo "${GITHUB_WORKSPACE} << GITHUB_WORKSPACE" 409 | echo "${GITHUB_SHA} << GITHUB_SHA" 410 | echo "${GITHUB_REF} << GITHUB_REF" 411 | echo "${GITHUB_SERVER_URL} << GITHUB_SERVER_URL" 412 | echo "${GITHUB_API_URL} << GITHUB_API_URL" 413 | echo "${GITHUB_GRAPHQL_URL} << GITHUB_GRAPHQL_URL" 414 | echo "${RUNNER_OS} << RUNNER_OS" 415 | echo "${RUNNER_TEMP} << RUNNER_TEMP" 416 | echo "${RUNNER_TOOL_CACHE} << RUNNER_TOOL_CACHE" 417 | echo "${GITHUB_ACTIONS} << GITHUB_ACTIONS" 418 | 419 | # review_snyk_opt: 420 | 421 | review_trivy_opt: 422 | needs: [myvars, build_stable, cd_stable] 423 | 424 | if: github.ref_type == 'tag' 425 | runs-on: ubuntu-latest 426 | steps: 427 | - uses: actions/checkout@v4 428 | with: 429 | fetch-depth: 0 430 | - uses: zcong1993/setup-timezone@v2.0.0 431 | with: 432 | timezone: America/Montreal 433 | 434 | - name: Checkpoint | Scanner Trivy for HIGH,CRITICAL CVEs and report (blocking) 435 | continue-on-error: true 436 | uses: aquasecurity/trivy-action@0.30.0 437 | with: 438 | image-ref: ${{ needs.myvars.outputs.TAG_DKR_VERSION_HASH_DATE }} 439 | exit-code: 0 440 | format: table 441 | ignore-unfixed: true 442 | vuln-type: "os,library" 443 | severity: "HIGH,CRITICAL" 444 | #output: "trivy-results.sarif" 445 | 446 | #- name: Upload Trivy scan results to GitHub Security tab 447 | # uses: github/codeql-action/upload-sarif@v3 448 | # if: always() 449 | # with: 450 | # sarif_file: "trivy-results.sarif" 451 | 452 | review_dockle: 453 | needs: [myvars, build_stable, cd_stable] 454 | if: github.ref_type == 'tag' 455 | runs-on: ubuntu-latest 456 | steps: 457 | - uses: actions/checkout@v4 458 | with: 459 | fetch-depth: 0 460 | - uses: zcong1993/setup-timezone@v2.0.0 461 | with: 462 | timezone: America/Montreal 463 | - name: Checkpoint | Scanner by Dockle 464 | run: | 465 | docker run --rm goodwithtech/dockle:latest ${{ needs.myvars.outputs.TAG_DKR_VERSION_HASH_DATE }} 466 | 467 | # review_superlinter_call: 468 | # needs: [myvars, build] 469 | # uses: firepress-org/reusable_workflows/.github/workflows/super-linter.yaml@master 470 | review_linter_opt: 471 | needs: [myvars, build_stable, cd_stable] 472 | if: github.ref_type == 'tag' 473 | runs-on: ubuntu-latest 474 | steps: 475 | - uses: actions/checkout@v4 476 | with: 477 | fetch-depth: 0 478 | - uses: zcong1993/setup-timezone@v2.0.0 479 | with: 480 | timezone: America/Montreal 481 | - name: Checkpoint | Linter 482 | continue-on-error: true 483 | uses: docker://ghcr.io/github/super-linter:slim-v4 484 | # github/super-linter@v4 485 | # docker://ghcr.io/github/super-linter:slim-v4 486 | env: 487 | DISABLE_ERRORS: true # Flag to have the linter complete with exit code 0 even if errors were detected. 488 | VALIDATE_ALL_CODEBASE: false 489 | VALIDATE_MARKDOWN: false 490 | VALIDATE_DOCKERFILE: false 491 | VALIDATE_JSCPD: false 492 | DEFAULT_BRANCH: edge 493 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 494 | 495 | review_inspect: 496 | needs: [myvars, build_stable, cd_stable] 497 | if: github.ref_type == 'tag' 498 | runs-on: ubuntu-latest 499 | steps: 500 | - uses: actions/checkout@v4 501 | with: 502 | fetch-depth: 0 503 | - uses: zcong1993/setup-timezone@v2.0.0 504 | with: 505 | timezone: America/Montreal 506 | - name: Login to DockerHub registry 507 | uses: docker/login-action@v3.4.0 508 | with: 509 | username: ${{ needs.myvars.outputs.DOCKERHUB_USER }} 510 | password: ${{ github.actor == 'dependabot[bot]' && secrets.DEPENDABOT_DOCKERHUB_PASSWORD || secrets.DOCKERHUB_PASSWORD }} 511 | - name: Login to GitHub registry 512 | uses: docker/login-action@v3.4.0 513 | with: 514 | registry: ghcr.io 515 | username: ${{ needs.myvars.outputs.GITHUB_ORG }} 516 | password: ${{ github.actor == 'dependabot[bot]' && secrets.TOKEN_GPR || secrets.TOKEN_GPR }} 517 | - name: Checkpoint | docker pull 518 | run: | 519 | docker pull ${{ needs.myvars.outputs.TAG_DKR_VERSION_HASH_DATE }} 520 | - name: Checkpoint | See platforms 521 | run: | 522 | docker run --rm mplatform/mquery ${{ needs.myvars.outputs.TAG_DKR_VERSION_HASH_DATE }} 523 | - name: Checkpoint | docker history 524 | run: | 525 | docker history --human ${{ needs.myvars.outputs.TAG_DKR_VERSION_HASH_DATE }} 526 | - name: Checkpoint | docker image inspect 527 | run: | 528 | docker image inspect ${{ needs.myvars.outputs.TAG_DKR_VERSION_HASH_DATE }} 529 | - name: Checkpoint | docker info 530 | run: | 531 | docker info 532 | - name: Checkpoint | docker version 533 | run: | 534 | docker version 535 | - name: Checkpoint | uname 536 | run: | 537 | uname -a && echo && echo 538 | 539 | review_lighthouse: 540 | needs: [myvars, build_stable, cd_stable] 541 | if: github.ref_type == 'tag' 542 | runs-on: ubuntu-latest 543 | steps: 544 | - uses: actions/checkout@v4 545 | with: 546 | fetch-depth: 0 547 | - uses: zcong1993/setup-timezone@v2.0.0 548 | with: 549 | timezone: America/Montreal 550 | 551 | - name: Checkpoint | Lighthouse 552 | uses: treosh/lighthouse-ci-action@12.1.0 553 | with: 554 | urls: | 555 | https://firepress.org/en/faq/#what-is-ghost 556 | https://firepress.org/en/our-mission/ 557 | #budgetPath: ./budget.json # test performance budgets 558 | uploadArtifacts: true # save results as an action artifacts 559 | temporaryPublicStorage: true # upload lighthouse report to the temporary storage 560 | 561 | # https://github.com/firepress-org/ghostfire/issues/408 562 | 563 | review_slack: 564 | needs: [myvars, build_stable, cd_stable] 565 | if: github.ref_type == 'tag' 566 | runs-on: ubuntu-latest 567 | steps: 568 | - uses: actions/checkout@v4 569 | with: 570 | fetch-depth: 0 571 | - uses: zcong1993/setup-timezone@v2.0.0 572 | with: 573 | timezone: America/Montreal 574 | 575 | - name: Notify on Slack 576 | run: | 577 | docker run --rm \ 578 | --name noti \ 579 | -e NOTI_MESSAGE='${{ needs.myvars.outputs._NOTI_MESSAGE }}' \ 580 | -e SLACK_CHANNEL="github-actions" \ 581 | -e SLACK_TOKEN_CRON="${{ secrets.TOKEN_SLACK }}" \ 582 | devmtl/noti:stable sh -c \ 583 | ' NOTI_SLACK_TOKEN="$SLACK_TOKEN_CRON" \ 584 | NOTI_SLACK_CHANNEL="$SLACK_CHANNEL" \ 585 | noti -k -m "$NOTI_MESSAGE" ' 586 | 587 | # ---------------------------------------------- 588 | # Actions after continuous_deployment 589 | # ---------------------------------------------- 590 | 591 | update_readme: 592 | needs: [myvars, build_stable, cd_stable] 593 | if: github.ref_type == 'tag' 594 | 595 | runs-on: ubuntu-latest 596 | steps: 597 | - uses: actions/checkout@v4 598 | with: 599 | fetch-depth: 0 600 | - uses: zcong1993/setup-timezone@v2.0.0 601 | with: 602 | timezone: America/Montreal 603 | 604 | - name: Update README on Dockerhub 605 | run: | 606 | docker run --rm \ 607 | -v $(pwd)/README.md:/data/README.md \ 608 | -e DOCKERHUB_USERNAME=${{ needs.myvars.outputs.DOCKERHUB_USER }} \ 609 | -e DOCKERHUB_PASSWORD=${{ secrets.DOCKERHUB_PASSWORD }} \ 610 | -e DOCKERHUB_REPO_PREFIX=${{ needs.myvars.outputs.DOCKERHUB_USER }} \ 611 | -e DOCKERHUB_REPO_NAME=${{ needs.myvars.outputs.APP_NAME }} \ 612 | devmtl/readme-to-dockerhub:stable 613 | 614 | comment_within_pr: 615 | needs: [myvars, build_stable, cd_stable] 616 | 617 | runs-on: ubuntu-latest 618 | steps: 619 | - uses: actions/checkout@v4 620 | with: 621 | fetch-depth: 0 622 | - uses: zcong1993/setup-timezone@v2.0.0 623 | with: 624 | timezone: America/Montreal 625 | 626 | - name: Find comment for image tags 627 | # If PR, put image tags in the PR comments 628 | # from https://github.com/marketplace/actions/create-or-update-comment 629 | uses: peter-evans/find-comment@v3 630 | if: github.event_name == 'pull_request' 631 | id: fc 632 | with: 633 | issue-number: ${{ github.event.pull_request.number }} 634 | comment-author: "github-actions[bot]" 635 | body-includes: Docker image tag(s) pushed 636 | 637 | # If PR, put image tags in the PR comments 638 | - name: Create or update comment for image tags 639 | uses: peter-evans/create-or-update-comment@v4 640 | if: github.event_name == 'pull_request' 641 | with: 642 | comment-id: ${{ steps.fc.outputs.comment-id }} 643 | issue-number: ${{ github.event.pull_request.number }} 644 | body: | 645 | Docker image tag(s) pushed: 646 | ```text 647 | ${{ needs.myvars.outputs.TAG_DKR_VERSION }} 648 | ${{ needs.myvars.outputs.TAG_DKR_BRANCH_NAME }} 649 | ${{ needs.myvars.outputs.TAG_DKR_VERSION_HASH_ONLY }} 650 | ${{ needs.myvars.outputs.TAG_DKR_VERSION_HASH_DATE }} 651 | ${{ needs.myvars.outputs.TAG_GPR_VERSION }} 652 | ${{ needs.myvars.outputs.TAG_GPR_BRANCH_NAME }} 653 | ${{ needs.myvars.outputs.TAG_GPR_VERSION_HASH_ONLY }} 654 | ${{ needs.myvars.outputs.TAG_GPR_VERSION_HASH_DATE }} 655 | ``` 656 | edit-mode: replace 657 | 658 | pr_label: 659 | needs: [myvars, build_stable, cd_stable] 660 | 661 | # https://docs.github.com/en/code-security/dependabot/working-with-dependabot/automating-dependabot-with-github-actions 662 | if: ${{ github.actor == 'dependabot[bot]' }} 663 | runs-on: ubuntu-latest 664 | steps: 665 | - uses: actions/checkout@v4 666 | with: 667 | fetch-depth: 0 668 | - uses: zcong1993/setup-timezone@v2.0.0 669 | with: 670 | timezone: America/Montreal 671 | 672 | - name: Login to DockerHub registry 673 | if: github.actor != 'dependabot[bot]' 674 | uses: docker/login-action@v3.4.0 675 | with: 676 | username: ${{ needs.myvars.outputs.DOCKERHUB_USER }} 677 | password: ${{ github.actor == 'dependabot[bot]' && secrets.DEPENDABOT_DOCKERHUB_PASSWORD || secrets.DOCKERHUB_PASSWORD }} 678 | 679 | - name: Add a label for PR opened by dependabot 680 | run: gh pr edit "$PR_URL" --add-label "dependabot" 681 | -------------------------------------------------------------------------------- /.github/workflows/linters/.dockerfilelintrc: -------------------------------------------------------------------------------- 1 | rules: 2 | missing_tag: off 3 | -------------------------------------------------------------------------------- /.github/workflows/linters/.hadolint.yaml: -------------------------------------------------------------------------------- 1 | ignored: 2 | - SC2128 #Expanding an array without an index only gives the first element. 3 | - DL3006 #image pin versions 4 | - DL3008 #apt pin versions 5 | - DL3017 #Do not use apk upgrade 6 | - DL3018 #Pin versions in apk add. Instead of `apk add ` use `apk add =` 7 | - DL3022 #outdated rule for COPY --from 8 | - DL3028 #gem install pin versions 9 | 10 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by https://www.toptal.com/developers/gitignore/api/node 2 | # Edit at https://www.toptal.com/developers/gitignore?templates=node 3 | 4 | ### Node ### 5 | # Logs 6 | logs 7 | *.log 8 | npm-debug.log* 9 | yarn-debug.log* 10 | yarn-error.log* 11 | lerna-debug.log* 12 | .pnpm-debug.log* 13 | 14 | # Diagnostic reports (https://nodejs.org/api/report.html) 15 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 16 | 17 | # Runtime data 18 | pids 19 | *.pid 20 | *.seed 21 | *.pid.lock 22 | 23 | # Directory for instrumented libs generated by jscoverage/JSCover 24 | lib-cov 25 | 26 | # Coverage directory used by tools like istanbul 27 | coverage 28 | *.lcov 29 | 30 | # nyc test coverage 31 | .nyc_output 32 | 33 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 34 | .grunt 35 | 36 | # Bower dependency directory (https://bower.io/) 37 | bower_components 38 | 39 | # node-waf configuration 40 | .lock-wscript 41 | 42 | # Compiled binary addons (https://nodejs.org/api/addons.html) 43 | build/Release 44 | 45 | # Dependency directories 46 | node_modules/ 47 | jspm_packages/ 48 | 49 | # Snowpack dependency directory (https://snowpack.dev/) 50 | web_modules/ 51 | 52 | # TypeScript cache 53 | *.tsbuildinfo 54 | 55 | # Optional npm cache directory 56 | .npm 57 | 58 | # Optional eslint cache 59 | .eslintcache 60 | 61 | # Microbundle cache 62 | .rpt2_cache/ 63 | .rts2_cache_cjs/ 64 | .rts2_cache_es/ 65 | .rts2_cache_umd/ 66 | 67 | # Optional REPL history 68 | .node_repl_history 69 | 70 | # Output of 'npm pack' 71 | *.tgz 72 | 73 | # Yarn Integrity file 74 | .yarn-integrity 75 | 76 | # parcel-bundler cache (https://parceljs.org/) 77 | .cache 78 | .parcel-cache 79 | 80 | # Next.js build output 81 | .next 82 | out 83 | 84 | # Nuxt.js build / generate output 85 | .nuxt 86 | dist 87 | 88 | # Gatsby files 89 | .cache/ 90 | # Comment in the public line in if your project uses Gatsby and not Next.js 91 | # https://nextjs.org/blog/next-9-1#public-directory-support 92 | # public 93 | 94 | # vuepress build output 95 | .vuepress/dist 96 | 97 | # Serverless directories 98 | .serverless/ 99 | 100 | # FuseBox cache 101 | .fusebox/ 102 | 103 | # DynamoDB Local files 104 | .dynamodb/ 105 | 106 | # TernJS port file 107 | .tern-port 108 | 109 | # Stores VSCode versions used for testing VSCode extensions 110 | .vscode-test 111 | 112 | # yarn v2 113 | .yarn/cache 114 | .yarn/unplugged 115 | .yarn/build-state.yml 116 | .yarn/install-state.gz 117 | .pnp.* 118 | 119 | ### Node Patch ### 120 | # Serverless Webpack directories 121 | .webpack/ 122 | 123 | # End of https://www.toptal.com/developers/gitignore/api/node 124 | 125 | # Files 126 | ############ 127 | .bashcheck.sh 128 | var-config.sh 129 | bashlava_help.md 130 | CLAUDE.md 131 | 132 | # Directories 133 | ############ 134 | tmp 135 | temp 136 | _ai_docs 137 | 138 | # Compiled source # 139 | ################### 140 | *.com 141 | *.class 142 | *.dll 143 | *.exe 144 | *.o 145 | *.so 146 | 147 | # Packages # 148 | ############ 149 | # it's better to unpack these files and commit the raw source 150 | # git has its own built in compression methods 151 | *.7z 152 | *.dmg 153 | *.gz 154 | *.iso 155 | *.jar 156 | *.rar 157 | *.tar 158 | *.zip 159 | 160 | # OS generated files # 161 | ###################### 162 | .DS_Store* 163 | .vscode 164 | .Trashes 165 | ehthumbs.db 166 | Thumbs.db 167 | .AppleDouble 168 | .LSOverride 169 | .metadata_never_index 170 | 171 | # Thumbnails 172 | ############ 173 | ._* 174 | 175 | # Icon must end with two \r 176 | ########################### 177 | Icon 178 | 179 | # Files that might appear in the root of a volume 180 | ################################################# 181 | .DocumentRevisions-V100 182 | .fseventsd 183 | .dbfseventsd 184 | .Spotlight-V100 185 | .TemporaryItems 186 | .Trashes 187 | .trash 188 | .VolumeIcon.icns 189 | .com.apple.timemachine.donotpresent 190 | .com.apple.timemachine.supported 191 | .PKInstallSandboxManager 192 | .PKInstallSandboxManager-SystemSoftware 193 | .file 194 | .hotfiles.btree 195 | .quota.ops.user 196 | .quota.user 197 | .quota.ops.group 198 | .quota.group 199 | .vol 200 | .efi 201 | 202 | # Directories potentially created on remote AFP share 203 | ##################################################### 204 | .AppleDB 205 | .AppleDesktop 206 | Network Trash Folder 207 | Temporary Items 208 | .apdisk 209 | .Mobile* 210 | .disk_* 211 | 212 | # Sherlock files 213 | ################ 214 | TheFindByContentFolder 215 | TheVolumeSettingsFolder 216 | .FBCIndex 217 | .FBCSemaphoreFile 218 | .FBCLockFolder 219 | -------------------------------------------------------------------------------- /.stickler.yml: -------------------------------------------------------------------------------- 1 | linters: 2 | shellcheck: 3 | shell: bash 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 FirePress 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |   2 | 3 |

4 | 5 | FirePress 6 | 7 |

8 | 9 |

10 | FirePress.org | 11 | play-with-ghost | 12 | GitHub | 13 | Twitter 14 |

15 |

16 | 17 |   18 | 19 | # [ghostfire](https://github.com/firepress-org/ghostfire) 20 | 21 | 22 | [![Build Status](https://img.shields.io/github/actions/workflow/status/firepress-org/ghostfire/ghostv5.yml?branch=master&label=Ghost%20V5%20alpine%20stable)](https://github.com/firepress-org/ghostfire/actions/workflows/ghostv5.yml) 23 | 24 | 25 | [![Docker Pulls](https://img.shields.io/docker/pulls/devmtl/ghostfire)](https://hub.docker.com/r/devmtl/ghostfire) 26 | [![Docker Image Size](https://img.shields.io/docker/image-size/devmtl/ghostfire/stable)](https://hub.docker.com/r/devmtl/ghostfire) 27 | [![Docker Image Version](https://img.shields.io/docker/v/devmtl/ghostfire?sort=semver)](https://hub.docker.com/r/devmtl/ghostfire) 28 | 29 | 30 | [![Ghost Version](https://img.shields.io/badge/Ghost-v5.120.4-brightgreen)](https://github.com/TryGhost/Ghost) 31 | [![Node Version](https://img.shields.io/badge/Node.js-20.19.2--alpine3.22-green)](https://nodejs.org/) 32 | [![Platform](https://img.shields.io/badge/platform-linux%2Famd64%20%7C%20linux%2Farm64%20%7C%20linux%2Farm%2Fv7-lightgrey)](https://hub.docker.com/r/devmtl/ghostfire) 33 | 34 | 35 | [![GitHub last commit](https://img.shields.io/github/last-commit/firepress-org/ghostfire)](https://github.com/firepress-org/ghostfire/commits) 36 | [![GitHub issues](https://img.shields.io/github/issues/firepress-org/ghostfire)](https://github.com/firepress-org/ghostfire/issues) 37 | [![License](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/firepress-org/ghostfire/blob/master/LICENSE) 38 | [![GitHub stars](https://img.shields.io/github/stars/firepress-org/ghostfire?style=social)](https://github.com/firepress-org/ghostfire) 39 | 40 |
41 | 42 | ## What is this? 43 | 44 | **What is Ghost?** — Ghost is an open-source software that lets you create your website with a blog. See the [FAQ section](https://play-with-ghost.com/ghost-themes/faq/#what-is-ghost) for more details. This project makes it run in a Docker image. 45 | 46 |
47 | 48 | ## Docker image features : 49 | 50 | - [x] **Multi-stage builds** with aggressive optimization 51 | - [x] **Security-focused**: Non-root user execution with `gosu` privilege dropping 52 | - [x] **Multi-architecture support**: `linux/amd64`, `linux/arm64`, `linux/arm/v7` 53 | - [x] **Production-ready**: `config.production.json` template with best practices 54 | - [x] **Alpine Linux base** for minimal attack surface (we don't maintain debian) 55 | - [x] **Health checks** with `curl` support 56 | - [x] **Labels** based on the OpenContainer standard 57 | - [x] **Conditional dependencies**: Smart installation of `sharp` and `sqlite3` 58 | - [x] **Enterprise-grade**: Optimized for Docker Swarm deployments 59 | 60 | We achieve significant size optimization through multi-stage builds and aggressive cleanup: 61 | 62 | ``` 63 | devmtl/ghostfire:stable ~320MB (optimized) 64 | ghost:5.x-alpine ~380MB (official) 65 | ``` 66 | 67 |
68 | 69 | ## GitHub Actions CI/CD : 70 | 71 | - [x] **Multi-architecture builds**: `linux/amd64`, `linux/arm64`, `linux/arm/v7` 72 | - [x] **Comprehensive security scanning**: Snyk, Dockle, Trivy vulnerability detection 73 | - [x] **Performance testing**: Lighthouse audits (localhost and online) 74 | - [x] **Quality assurance**: Linting using `super-linter` 75 | - [x] **Automated deployment**: Continuous deployment to Docker Swarm clusters 76 | - [x] **Smart caching**: Build cache optimization for faster CI/CD 77 | - [x] **Notification system**: Slack notifications for build status 78 | - [x] **Shared variables**: Efficient job coordination and data sharing 79 | - [x] **Best practices**: Following [enterprise DevOps standards](https://firepress.org/en/how-do-we-update-hundreds-of-ghosts-websites-on-docker-swarm/) 80 | - [x] **Extreme visibility**: Comprehensive logging and monitoring during builds 81 | 82 | ![CI_2021-10-03_17h42](https://user-images.githubusercontent.com/6694151/135772462-0c39fe73-be9e-4aa3-8103-b1c849c0c41f.jpg) 83 | 84 |
85 | 86 | ## Live Demo 87 | 88 | Want to try Ghost quickly? This is for you! 89 | 90 | [play-with-ghost.com](https://play-with-ghost.com/) is a playground to learn about Ghost. What's remarkable here, is that you have the option to log into the admin panel of each live demo available, by using dummy credentials. 91 | 92 | In short, you can try Ghost on the spot without having to sign-up! 93 | 94 |
95 | 96 | [![pwg-video-preview-e](https://user-images.githubusercontent.com/6694151/50233512-9bbc8a80-0381-11e9-83bb-f29a67000378.jpg) 97 | ](https://play-with-ghost.com/) 98 | 99 |
100 | 101 | #### Continuous integration 102 | 103 | See [Github Actions sections](https://github.com/firepress-org/ghostfire/actions) 104 | 105 | At this point, this docker image has been pulled more than **11 millions of time**! 106 | 107 | ![docker-hub](https://user-images.githubusercontent.com/6694151/53067692-4c8af700-34a3-11e9-9fcf-9c7ad169a91b.jpg) 108 | 109 | ## Quick Start 110 | 111 | #### Option #1 (basic run) 112 | 113 | ```bash 114 | GHOSTFIRE_IMG="devmtl/ghostfire:stable" 115 | 116 | docker run -d \ 117 | --name ghostblog \ 118 | -p 2368:2368 \ 119 | -e url=http://localhost:2368 \ 120 | ${GHOSTFIRE_IMG} 121 | ``` 122 | 123 | #### Option #2 (production with persistent data) 124 | 125 | ```bash 126 | GHOSTFIRE_IMG="devmtl/ghostfire:stable" 127 | 128 | docker run -d \ 129 | --name ghostblog \ 130 | -p 2368:2368 \ 131 | -e url=http://localhost:2368 \ 132 | -v /myuser/localpath/ghost/content:/var/lib/ghost/content \ 133 | -v /myuser/localpath/ghost/config.production.json:/var/lib/ghost/config.production.json \ 134 | ${GHOSTFIRE_IMG} 135 | ``` 136 | 137 | #### Option #3 (development with health checks) 138 | 139 | ```bash 140 | GHOSTFIRE_IMG="devmtl/ghostfire:stable" 141 | 142 | docker run -d \ 143 | --name ghostblog \ 144 | -p 2368:2368 \ 145 | -e url=http://localhost:2368 \ 146 | -e NODE_ENV=development \ 147 | --health-cmd="curl -f http://localhost:2368/ || exit 1" \ 148 | --health-interval=30s \ 149 | --health-timeout=10s \ 150 | --health-retries=3 \ 151 | -v $(pwd)/content:/var/lib/ghost/content \ 152 | ${GHOSTFIRE_IMG} 153 | ``` 154 | 155 | To configure the `config.production.json` refer the [ghost docs](https://docs.ghost.org/concepts/config/). 156 | 157 | #### master branch (stable) tags 🐳 158 | 159 | For the **stable** branch, I recommend using the tag from the **first line**: 160 | 161 | ``` 162 | devmtl/ghostfire:stable_5.120.4__ 163 | devmtl/ghostfire:stable_5.120.4 164 | devmtl/ghostfire:stable 165 | ``` 166 | 167 | Find the latest tags on **DockerHub** here: 168 | [https://hub.docker.com/r/devmtl/ghostfire/tags/](https://hub.docker.com/r/devmtl/ghostfire/tags/) 169 | 170 | #### edge branch (dev) tags 🐳 171 | 172 | This is reserved for development and testing. 173 | 174 | ``` 175 | devmtl/ghostfire:edge_5.120.4__ 176 | devmtl/ghostfire:edge_5.120.4 177 | devmtl/ghostfire:edge 178 | ``` 179 | 180 | ## Architecture & Security 181 | 182 | ### Multi-Stage Build Process 183 | 184 | Our Dockerfile uses a sophisticated 4-stage build process: 185 | 186 | 1. **`mynode`** - Base Node.js environment with security tools (gosu, timezone setup) 187 | 2. **`debug`** - Package version debugging and validation layer 188 | 3. **`builder`** - Ghost installation and native dependency compilation 189 | 4. **`final`** - Minimal runtime image with only necessary components 190 | 191 | ### Security Features 192 | 193 | - **Non-root execution**: Runs as `node` user for enhanced security 194 | - **Privilege dropping**: Uses `gosu` for secure step-down from root 195 | - **File permissions**: Proper ownership and permission management 196 | - **Minimal attack surface**: Alpine Linux base with aggressive cleanup 197 | - **Vulnerability scanning**: Integrated security scanning in CI/CD 198 | 199 | ### Volume Management 200 | 201 | Critical paths for data persistence: 202 | 203 | ```bash 204 | /var/lib/ghost/content # All Ghost content, themes, uploads 205 | /var/lib/ghost/config.production.json # Runtime configuration 206 | /var/lib/ghost/content/logs # Application logs 207 | /var/lib/ghost/content/data/ghost.db # SQLite database 208 | ``` 209 | 210 |
211 | 212 | ## DevOps best practices 213 | 214 | Let's understand our processes. In this post « [How we update hundreds of Ghost's websites on Docker Swarm?](https://firepress.org/en/how-we-update-hundreds-of-ghosts-websites-on-docker-swarm/) », we explain how we deploy Ghost in production and which best practices we do follow. 215 | 216 | ## Enhanced unit tests during the CI 217 | 218 | ![unit-test-a](https://user-images.githubusercontent.com/6694151/61649557-b80a0800-ac7f-11e9-85b7-425e5456cb2d.jpg) 219 |
220 | ![unit-test-b](https://user-images.githubusercontent.com/6694151/61649559-b80a0800-ac7f-11e9-9154-cd6f5264af71.jpg) 221 |
222 | ![unit-test-c](https://user-images.githubusercontent.com/6694151/61649558-b80a0800-ac7f-11e9-9638-4b3241e4dcee.jpg) 223 | 224 |
225 | 226 | ## Developing Ghost themes locally 227 | 228 | I open-sourced [my setup here](https://github.com/firepress-org/ghost-local-dev-in-docker). It’s a workflow to run Ghost locally within a Docker container. Once your local paths are defined, it’s enjoyable and easy to work **between many themes**. 229 | 230 |
231 | 232 | ## Configuration & Database 233 | 234 | ### Database Support 235 | 236 | - **SQLite** (default): Zero-configuration, file-based database at `/var/lib/ghost/content/data/ghost.db` 237 | - **MySQL**: Full support with connection configuration in `config.production.json` 238 | 239 | ### Configuration Management 240 | 241 | Ghost configuration is handled via `config.production.json` with these key areas: 242 | 243 | - **Database connection**: SQLite default or MySQL configuration 244 | - **Mail configuration**: SMTP/Mailgun templates provided 245 | - **Logging**: 7-day retention with 5-day rotation 246 | - **Content paths**: Mapped to `/var/lib/ghost/content` 247 | - **URL configuration**: Flexible URL and SSL settings 248 | 249 | ### Version Information 250 | 251 | **Current Stack:** 252 | - **Ghost**: v5.120.4 (latest stable) 253 | - **Node.js**: 20.19.2 on Alpine Linux 3.22 254 | - **Ghost CLI**: v1.27.0 255 | 256 | Check versions in running container: 257 | ```bash 258 | docker exec node --version 259 | docker exec ghost --version 260 | ``` 261 | 262 | ### Migration Notes 263 | 264 | **Path consistency across versions:** 265 | ``` 266 | - Ghost 5.x.x: /var/lib/ghost/content ✅ (current) 267 | - Ghost 4.x.x: /var/lib/ghost/content 268 | - Ghost 3.x.x: /var/lib/ghost/content 269 | - Ghost 2.x.x: /var/lib/ghost/content 270 | - Ghost 1.x.x: /var/lib/ghost/content 271 | - Ghost 0.11.x: /var/lib/ghost (deprecated) 272 | ``` 273 | 274 | 275 |
276 | 277 | ## FirePress Hosting 278 | 279 | At FirePress we empower entrepreneurs and small organizations to create their websites on top of [Ghost](https://firepress.org/en/faq/#what-is-ghost). 280 | 281 | At the moment, our **pricing** for hosting one Ghost website is $15 (Canadian dollars). This price will be only available for our first 100 new clients, starting May 1st, 2019 🙌. [See our pricing section](https://firepress.org/en/pricing/) for details. 282 | 283 | More details [about this announcement](https://forum.ghost.org/t/host-your-ghost-website-on-firepress/7092/1) on Ghost's forum. 284 | 285 |
286 | 287 | ## Contributing 288 | 289 | The power of communities pull request and forks means that `1 + 1 = 3`. You can help to make this repo a better one! Here is how: 290 | 291 | 1. Fork it 292 | 2. Create your feature branch: `git checkout -b my-new-feature` 293 | 3. Commit your changes: `git commit -am 'Add some feature'` 294 | 4. Push to the branch: `git push origin my-new-feature` 295 | 5. Submit a pull request 296 | 297 | Check this post for more details: [Contributing to our Github project](https://pascalandy.com/blog/contributing-to-our-github-project/). Also, by contributing you agree to the [Contributor Code of Conduct on GitHub](https://pascalandy.com/blog/contributor-code-of-conduct-on-github/). It's plain common sense really. 298 | 299 |
300 | 301 | ## License 302 | 303 | - This git repo is under the **MIT** license. [Find it here](https://github.com/firepress-org/ghostfire/blob/master/LICENSE). 304 | - The Ghost's software is under the **MIT** license. [Find it here](https://ghost.org/license/). 305 | 306 |
307 | 308 | ## Sources & Fork 309 | 310 | - This Git repo is available at [https://github.com/firepress-org/ghostfire](https://github.com/firepress-org/ghostfire) 311 | - Forked from the [official](https://github.com/docker-library/ghost/) Ghost image 312 | 313 |
314 | 315 | ## Why all this work? 316 | 317 | Our [mission](https://firepress.org/en/our-mission/) is to empower freelancers and small organizations to build an outstanding mobile-first website. 318 | 319 | Because we believe your website should speak up in your name, we consider our mission completed once your site has become your impresario. 320 | 321 | For more info about the man behind the startup, check out my [now page](https://pascalandy.com/blog/now/). You can also follow me on Twitter [@askpascalandy](https://twitter.com/askpascalandy). 322 | 323 | — The FirePress Team 🔥📰 324 | -------------------------------------------------------------------------------- /v5/.dockerignore: -------------------------------------------------------------------------------- 1 | # Version control 2 | .git 3 | .gitignore 4 | .gitattributes 5 | 6 | # Documentation 7 | README.md 8 | CHANGELOG.md 9 | LICENSE 10 | *.md 11 | 12 | # Docker files 13 | Dockerfile* 14 | .dockerignore 15 | docker-compose* 16 | .docker 17 | 18 | # Node.js 19 | node_modules 20 | npm-debug.log 21 | npm-debug 22 | yarn-debug.log* 23 | yarn-error.log* 24 | .npm 25 | .yarn 26 | .pnpm-debug.log* 27 | 28 | # Build artifacts 29 | dist 30 | build 31 | coverage 32 | .nyc_output 33 | 34 | # Cache directories 35 | .cache 36 | .parcel-cache 37 | .next 38 | .nuxt 39 | 40 | 41 | # IDE and editor files 42 | .idea 43 | *.swp 44 | *.swo 45 | *~ 46 | 47 | # OS generated files 48 | .DS_Store 49 | .DS_Store? 50 | ._* 51 | .Spotlight-V100 52 | .Trashes 53 | ehthumbs.db 54 | Thumbs.db 55 | 56 | # Test files 57 | test 58 | tests 59 | __tests__ 60 | *.test.js 61 | *.spec.js 62 | 63 | # CI/CD 64 | .github 65 | .gitlab-ci.yml 66 | .travis.yml 67 | .circleci 68 | 69 | # Logs 70 | logs 71 | *.log 72 | 73 | # Temporary files 74 | tmp 75 | temp 76 | .tmp 77 | -------------------------------------------------------------------------------- /v5/Dockerfile: -------------------------------------------------------------------------------- 1 | # syntax=docker/dockerfile:1 2 | 3 | # ---------------------------------------------- 4 | # 1a) ENV variables (core for all our projects at FirePress) 5 | # ---------------------------------------------- 6 | ARG APP_NAME="ghostfire" 7 | ARG VERSION="5.112.0" 8 | 9 | ARG GITHUB_USER="firepress-org" 10 | ARG DEFAULT_BRANCH="master" 11 | ARG GITHUB_ORG="firepress-org" 12 | ARG DOCKERHUB_USER="devmtl" 13 | ARG GITHUB_REGISTRY="registry" 14 | 15 | # ---------------------------------------------- 16 | # 1b) ENV variables (for this project) 17 | # Various docs about our Dockerfile - https://github.com/firepress-org/ghostfire/issues/529 18 | # ---------------------------------------------- 19 | ARG GHOST_CLI_VERSION="1.27.0" 20 | ARG NODE_VERSION="20.19.2-alpine3.22" 21 | ARG BASE_OS="alpine" 22 | ARG USER="node" 23 | 24 | # ---------------------------------------------- 25 | # 2) LAYER to manage base image versioning 26 | # ---------------------------------------------- 27 | FROM node:${NODE_VERSION} AS mynode 28 | 29 | ARG VERSION 30 | ARG GHOST_CLI_VERSION 31 | ARG USER 32 | ARG NODE_VERSION 33 | ARG ALPINE_VERSION 34 | 35 | ENV GHOST_INSTALL="/var/lib/ghost" \ 36 | GHOST_CONTENT="/var/lib/ghost/content" \ 37 | NODE_ENV="production" \ 38 | USER="${USER}" \ 39 | NODE_VERSION="${NODE_VERSION}" \ 40 | VERSION="${VERSION}" \ 41 | GHOST_CLI_VERSION="${GHOST_CLI_VERSION}" 42 | 43 | LABEL org.opencontainers.image.authors="Pascal Andy https://firepress.org/en/contact/" \ 44 | org.opencontainers.image.vendor="https://firepress.org/" \ 45 | org.opencontainers.image.created="${BUILD_DATE}" \ 46 | org.opencontainers.image.revision="${VCS_REF}" \ 47 | org.opencontainers.image.title="Ghost" \ 48 | org.opencontainers.image.description="Docker image for Ghost ${VERSION}" \ 49 | org.opencontainers.image.url="https://hub.docker.com/r/devmtl/ghostfire/tags/" \ 50 | org.opencontainers.image.source="https://github.com/firepress-org/ghostfire" \ 51 | org.opencontainers.image.licenses="GNUv3 https://github.com/pascalandy/GNU-GENERAL-PUBLIC-LICENSE/blob/master/LICENSE.md" \ 52 | com.firepress.image.ghost_cli_version="${GHOST_CLI_VERSION}" \ 53 | com.firepress.image.user="${USER}" \ 54 | com.firepress.image.node_env="${NODE_ENV}" \ 55 | com.firepress.image.node_version="${NODE_VERSION}" \ 56 | com.firepress.image.base_os="${BASE_OS}" \ 57 | com.firepress.image.schema_version="1.0" 58 | 59 | # Install gosu for easy step-down from root 60 | ENV GOSU_VERSION 1.17 61 | RUN set -eux; \ 62 | apk add --no-cache --virtual .gosu-deps \ 63 | ca-certificates \ 64 | dpkg \ 65 | gnupg \ 66 | ; \ 67 | dpkgArch="$(dpkg --print-architecture | awk -F- '{ print $NF }')"; \ 68 | wget -O /usr/local/bin/gosu "https://github.com/tianon/gosu/releases/download/$GOSU_VERSION/gosu-$dpkgArch"; \ 69 | wget -O /usr/local/bin/gosu.asc "https://github.com/tianon/gosu/releases/download/$GOSU_VERSION/gosu-$dpkgArch.asc"; \ 70 | export GNUPGHOME="$(mktemp -d)"; \ 71 | gpg --batch --keyserver hkps://keys.openpgp.org --recv-keys B42F6819007F00F88E364FD4036A9C25BF357DD4; \ 72 | gpg --batch --verify /usr/local/bin/gosu.asc /usr/local/bin/gosu; \ 73 | gpgconf --kill all; \ 74 | rm -rf "$GNUPGHOME" /usr/local/bin/gosu.asc; \ 75 | apk del --no-network .gosu-deps; \ 76 | chmod +x /usr/local/bin/gosu; \ 77 | gosu --version; \ 78 | gosu nobody true 79 | 80 | # Add the backwards compatibility with the official dockerfile from dockerhub 81 | RUN set -eux; ln -svf gosu /usr/local/bin/su-exec; su-exec nobody true 82 | 83 | # Add bash and set timezone 84 | RUN apk add --no-cache bash curl tzdata && \ 85 | cp /usr/share/zoneinfo/America/New_York /etc/localtime && \ 86 | echo "America/New_York" > /etc/timezone && \ 87 | apk del tzdata && \ 88 | rm -rvf /var/cache/apk/* /tmp/* 89 | 90 | # ---------------------------------------------- 91 | # 3) LAYER debug 92 | # If a package crash on layers 4 or 5, we don't know which one crashed. 93 | # This layer reveal package(s) versions and keep a trace in the CI's logs. 94 | # ---------------------------------------------- 95 | FROM mynode AS debug 96 | RUN apk upgrade 97 | 98 | # ---------------------------------------------- 99 | # 4) LAYER builder 100 | # ---------------------------------------------- 101 | FROM mynode AS builder 102 | 103 | # Use bash for shell commands, and fail builds if any command in a pipeline fails. 104 | SHELL ["/bin/bash", "-o", "pipefail", "-c"] 105 | 106 | RUN set -eux; \ 107 | npm install -g "ghost-cli@$GHOST_CLI_VERSION"; \ 108 | npm cache clean --force; \ 109 | mkdir -p "$GHOST_INSTALL"; \ 110 | chown node:node "$GHOST_INSTALL"; \ 111 | apkDel=; \ 112 | echo "Installing Ghost version: $VERSION"; \ 113 | installCmd='gosu node ghost install "$VERSION" --db mysql --dbhost mysql --no-prompt --no-stack --no-setup --dir "$GHOST_INSTALL"'; \ 114 | if ! eval "$installCmd"; then \ 115 | echo "Initial Ghost installation failed, installing build dependencies..."; \ 116 | virtual='.build-deps-ghost'; \ 117 | apkDel="$apkDel $virtual"; \ 118 | apk add --no-cache --virtual "$virtual" g++ linux-headers make python3 pkgconfig libc6-compat; \ 119 | echo "Retrying Ghost installation with build dependencies..."; \ 120 | eval "$installCmd"; \ 121 | fi; \ 122 | cd "$GHOST_INSTALL"; \ 123 | gosu node ghost config --no-prompt --ip '::' --port 2368 --url 'http://localhost:2368'; \ 124 | gosu node ghost config paths.contentPath "$GHOST_CONTENT"; \ 125 | gosu node ln -s config.production.json "$GHOST_INSTALL/config.development.json"; \ 126 | readlink -f "$GHOST_INSTALL/config.development.json"; \ 127 | mv "$GHOST_CONTENT" "$GHOST_INSTALL/content.orig"; \ 128 | mkdir -p "$GHOST_CONTENT"; \ 129 | chown node:node "$GHOST_CONTENT"; \ 130 | chmod 1777 "$GHOST_CONTENT"; \ 131 | cd "$GHOST_INSTALL/current"; \ 132 | packages="$(node -p ' \ 133 | var ghost = require("./package.json"); \ 134 | var sharpVersion = ""; \ 135 | try { \ 136 | var transform = require("./node_modules/@tryghost/image-transform/package.json"); \ 137 | sharpVersion = transform.optionalDependencies["sharp"] || transform.dependencies["sharp"]; \ 138 | } catch(e) { \ 139 | try { \ 140 | sharpVersion = ghost.optionalDependencies["sharp"] || ghost.dependencies["sharp"]; \ 141 | } catch(e2) { \ 142 | sharpVersion = "latest"; \ 143 | } \ 144 | } \ 145 | var sqlite3Version = ghost.optionalDependencies["sqlite3"] || ghost.dependencies["sqlite3"] || "latest"; \ 146 | [ \ 147 | "sharp@" + sharpVersion, \ 148 | "sqlite3@" + sqlite3Version, \ 149 | ].join(" ") \ 150 | ')"; \ 151 | echo "Detected packages to install: $packages"; \ 152 | if echo "$packages" | grep 'undefined'; then \ 153 | echo "Error: undefined package version detected"; \ 154 | exit 1; \ 155 | fi; \ 156 | for package in $packages; do \ 157 | echo "Installing package: $package"; \ 158 | installCmd='gosu node yarn add "$package" --force'; \ 159 | if ! eval "$installCmd"; then \ 160 | echo "Yarn installation failed, trying with npm: $package"; \ 161 | npmInstallCmd='gosu node npm install "$package" --save --force'; \ 162 | if ! eval "$npmInstallCmd"; then \ 163 | echo "Package installation failed, installing build dependencies for: $package"; \ 164 | virtualPackages='g++ make python3 pkgconfig vips-dev libc6-compat'; \ 165 | virtual=".build-deps-${package%%@*}"; \ 166 | apkDel="$apkDel $virtual"; \ 167 | apk add --no-cache --virtual "$virtual" $virtualPackages; \ 168 | echo "Retrying yarn installation with build-from-source: $package"; \ 169 | if ! eval "$installCmd --build-from-source"; then \ 170 | echo "Retrying npm installation with build-from-source: $package"; \ 171 | eval "$npmInstallCmd --build-from-source"; \ 172 | fi; \ 173 | fi; \ 174 | fi; \ 175 | echo "Successfully installed: $package"; \ 176 | done; \ 177 | if [ -n "$apkDel" ]; then \ 178 | apk del --no-network $apkDel; \ 179 | fi; \ 180 | gosu node yarn cache clean; \ 181 | gosu node npm cache clean --force; \ 182 | npm cache clean --force; \ 183 | rm -rv /tmp/yarn* /tmp/v8* 184 | 185 | # ---------------------------------------------- 186 | # 5) LAYER final 187 | # ---------------------------------------------- 188 | FROM mynode AS final 189 | 190 | COPY --chown="${USER}":"${USER}" /v5/docker-entrypoint.sh /usr/local/bin 191 | COPY --from=builder --chown="${USER}":"${USER}" "${GHOST_INSTALL}" "${GHOST_INSTALL}" 192 | 193 | WORKDIR "${GHOST_INSTALL}" 194 | VOLUME "${GHOST_CONTENT}" 195 | USER "${USER}" 196 | EXPOSE 2368 197 | 198 | # HEALTHCHECK must be done during the runtime 199 | 200 | ENTRYPOINT [ "docker-entrypoint.sh" ] 201 | CMD [ "node", "current/index.js" ] -------------------------------------------------------------------------------- /v5/config.production.json: -------------------------------------------------------------------------------- 1 | { 2 | "url": "http://localhost:2368/", 3 | "server": { 4 | "port": "2368", 5 | "host": "0.0.0.0" 6 | }, 7 | "database": { 8 | "client": "sqlite3", 9 | "connection": { 10 | "filename": "/var/lib/ghost/content/data/ghost.db" 11 | } 12 | }, 13 | "mail": { 14 | "from": "'Our Company Team' ", 15 | "transport": "SMTP", 16 | "options": { 17 | "service": "Mailgun", 18 | "host": "smtp.mailgun.org", 19 | "auth": { 20 | "user": "postmaster@thisisus.com", 21 | "pass": "6546545466254456654-4566544-654456654" 22 | } 23 | } 24 | }, 25 | "logging": { 26 | "path": "/var/lib/ghost/content/logs", 27 | "level": "error", 28 | "rotation": { 29 | "enabled": true, 30 | "count": 7, 31 | "period": "5d" 32 | }, 33 | "transports": ["stdout", "file"] 34 | }, 35 | "caching": { 36 | "frontend": { 37 | "maxAge": 1 38 | } 39 | }, 40 | "process": "local", 41 | "paths": { 42 | "contentPath": "/var/lib/ghost/content" 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /v5/docker-entrypoint.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | 4 | # allow the container to be started with `--user` 5 | if [[ "$*" == node*current/index.js* ]] && [ "$(id -u)" = '0' ]; then 6 | find "$GHOST_CONTENT" \! -user node -exec chown node '{}' + 7 | exec gosu node "$BASH_SOURCE" "$@" 8 | fi 9 | 10 | if [[ "$*" == node*current/index.js* ]]; then 11 | baseDir="$GHOST_INSTALL/content.orig" 12 | for src in "$baseDir"/*/ "$baseDir"/themes/*; do 13 | src="${src%/}" 14 | target="$GHOST_CONTENT/${src#$baseDir/}" 15 | mkdir -p "$(dirname "$target")" 16 | if [ ! -e "$target" ]; then 17 | tar -cC "$(dirname "$src")" "$(basename "$src")" | tar -xC "$(dirname "$target")" 18 | fi 19 | done 20 | fi 21 | 22 | exec "$@" 23 | -------------------------------------------------------------------------------- /v5/test-build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -euo pipefail 3 | 4 | echo "=== Testing Ghostfire Docker Build ===" 5 | echo "Building Docker image locally..." 6 | 7 | # Build the image 8 | docker build -t ghostfire:test-build . 9 | 10 | echo "=== Build completed successfully ===" 11 | echo "Testing container startup..." 12 | 13 | # Test the container 14 | docker run -d --name ghostfire-test -p 2368:2368 \ 15 | -e url=http://localhost:2368 \ 16 | -e NODE_ENV=production \ 17 | -e database__client=sqlite3 \ 18 | -e database__connection__filename=/var/lib/ghost/content/data/ghost.db \ 19 | ghostfire:test-build 20 | 21 | echo "Waiting for Ghost to start..." 22 | sleep 10 23 | 24 | # Check if Ghost is running 25 | if curl -f http://localhost:2368 > /dev/null 2>&1; then 26 | echo "✅ Ghost is running successfully!" 27 | else 28 | echo "❌ Ghost failed to start" 29 | docker logs ghostfire-test 30 | exit 1 31 | fi 32 | 33 | # Cleanup 34 | echo "Cleaning up test container..." 35 | docker stop ghostfire-test 36 | docker rm ghostfire-test 37 | 38 | echo "=== Test completed successfully ===" 39 | echo "You can now push your changes to trigger GitHub Actions" -------------------------------------------------------------------------------- /v5/test/config.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | globalTests+=( 4 | no-hard-coded-passwords 5 | override-cmd 6 | ) 7 | 8 | # for "explicit" images, only run tests that are explicitly specified for that image/variant 9 | explicitTests+=( 10 | [:onbuild]=1 11 | ) 12 | imageTests[:onbuild]+=' 13 | override-cmd 14 | ' 15 | 16 | testAlias+=( 17 | [amazoncorretto]='openjdk' 18 | [adoptopenjdk]='openjdk' 19 | [sapmachine]='openjdk' 20 | [iojs]='node' 21 | [jruby]='ruby' 22 | [pypy]='python' 23 | 24 | [ubuntu]='debian' 25 | [ubuntu-debootstrap]='debian' 26 | 27 | [mariadb]='mysql' 28 | [percona]='mysql' 29 | [percona:psmdb]='mongo' 30 | 31 | [hola-mundo]='hello-world' 32 | [hello-seattle]='hello-world' 33 | ) 34 | 35 | imageTests+=( 36 | [aerospike]=' 37 | ' 38 | [busybox]=' 39 | ' 40 | [cassandra]=' 41 | cassandra-basics 42 | ' 43 | [celery]=' 44 | ' 45 | [clojure]=' 46 | ' 47 | [crate]=' 48 | ' 49 | [composer]=' 50 | composer 51 | ' 52 | [convertigo]=' 53 | convertigo-hello-world 54 | ' 55 | [dart]=' 56 | dart-hello-world 57 | ' 58 | [debian]=' 59 | debian-apt-get 60 | ' 61 | [docker:dind]=' 62 | docker-dind 63 | docker-registry-push-pull 64 | ' 65 | [django]=' 66 | ' 67 | [eclipse-mosquitto]=' 68 | eclipse-mosquitto-basics 69 | ' 70 | [elasticsearch]=' 71 | elasticsearch-basics 72 | ' 73 | [elixir]=' 74 | elixir-hello-world 75 | ' 76 | [erlang]=' 77 | erlang-hello-world 78 | ' 79 | [fsharp]=' 80 | fsharp-hello-world 81 | ' 82 | [gcc]=' 83 | gcc-c-hello-world 84 | gcc-cpp-hello-world 85 | golang-hello-world 86 | ' 87 | [ghost]=' 88 | ghost-basics 89 | ' 90 | [golang]=' 91 | golang-hello-world 92 | ' 93 | [haproxy]=' 94 | haproxy-basics 95 | ' 96 | [haskell]=' 97 | haskell-cabal 98 | haskell-stack 99 | haskell-ghci 100 | haskell-runhaskell 101 | ' 102 | [haxe]=' 103 | haxe-hello-world 104 | haxe-haxelib-install 105 | ' 106 | [hylang]=' 107 | hylang-sh 108 | hylang-hello-world 109 | ' 110 | [jetty]=' 111 | jetty-hello-web 112 | ' 113 | [julia]=' 114 | julia-hello-world 115 | julia-downloads 116 | ' 117 | [logstash]=' 118 | logstash-basics 119 | ' 120 | [memcached]=' 121 | memcached-basics 122 | ' 123 | [mongo]=' 124 | mongo-basics 125 | mongo-auth-basics 126 | mongo-tls-basics 127 | mongo-tls-auth 128 | ' 129 | [monica]=' 130 | monica-cli 131 | ' 132 | [monica:apache]=' 133 | monica-apache-run 134 | ' 135 | [monica:fpm]=' 136 | monica-fpm-run 137 | ' 138 | [monica:fpm-alpine]=' 139 | monica-fpm-run 140 | ' 141 | [mongo-express]=' 142 | mongo-express-run 143 | ' 144 | [mono]=' 145 | ' 146 | [mysql]=' 147 | mysql-basics 148 | mysql-initdb 149 | mysql-log-bin 150 | ' 151 | [nextcloud]=' 152 | nextcloud-cli-mysql 153 | nextcloud-cli-postgres 154 | nextcloud-cli-sqlite 155 | ' 156 | [nextcloud:apache]=' 157 | nextcloud-apache-run 158 | ' 159 | [nextcloud:fpm]=' 160 | nextcloud-fpm-run 161 | ' 162 | [node]=' 163 | node-hello-world 164 | ' 165 | [nuxeo]=' 166 | nuxeo-conf 167 | nuxeo-basics 168 | ' 169 | [openjdk]=' 170 | java-hello-world 171 | java-uimanager-font 172 | java-ca-certificates 173 | ' 174 | [open-liberty]=' 175 | open-liberty-hello-world 176 | ' 177 | [percona]=' 178 | percona-tokudb 179 | percona-rocksdb 180 | ' 181 | [perl]=' 182 | perl-hello-world 183 | ' 184 | [php]=' 185 | php-ext-install 186 | php-hello-world 187 | php-argon2 188 | ' 189 | [php:apache]=' 190 | php-apache-hello-web 191 | ' 192 | [php:fpm]=' 193 | php-fpm-hello-web 194 | ' 195 | [plone]=' 196 | plone-basics 197 | plone-addons 198 | plone-cors 199 | plone-versions 200 | plone-zeoclient 201 | plone-zeosite 202 | ' 203 | [postgres]=' 204 | postgres-basics 205 | postgres-initdb 206 | ' 207 | [python]=' 208 | python-hy 209 | python-imports 210 | python-pip-requests-ssl 211 | python-sqlite3 212 | python-stack-size 213 | ' 214 | [rabbitmq]=' 215 | rabbitmq-basics 216 | rabbitmq-tls 217 | ' 218 | [r-base]=' 219 | ' 220 | [rails]=' 221 | ' 222 | [rapidoid]=' 223 | rapidoid-hello-world 224 | rapidoid-load-balancer 225 | ' 226 | [redis]=' 227 | redis-basics 228 | redis-basics-tls 229 | redis-basics-config 230 | redis-basics-persistent 231 | ' 232 | [redmine]=' 233 | redmine-basics 234 | ' 235 | [registry]=' 236 | docker-registry-push-pull 237 | ' 238 | [rethinkdb]=' 239 | ' 240 | [ruby]=' 241 | ruby-hello-world 242 | ruby-standard-libs 243 | ruby-gems 244 | ruby-bundler 245 | ruby-nonroot 246 | ruby-binstubs 247 | ' 248 | [rust]=' 249 | rust-hello-world 250 | ' 251 | [silverpeas]=' 252 | silverpeas-basics 253 | ' 254 | [spiped]=' 255 | spiped-basics 256 | ' 257 | [swipl]=' 258 | swipl-modules 259 | ' 260 | [swift]=' 261 | swift-hello-world 262 | ' 263 | [tomcat]=' 264 | tomcat-hello-world 265 | ' 266 | [varnish]=' 267 | varnish 268 | ' 269 | [wordpress:apache]=' 270 | wordpress-apache-run 271 | ' 272 | [wordpress:fpm]=' 273 | wordpress-fpm-run 274 | ' 275 | [znc]=' 276 | znc-basics 277 | ' 278 | [zookeeper]=' 279 | zookeeper-basics 280 | ' 281 | ) 282 | 283 | globalExcludeTests+=( 284 | # single-binary images 285 | [hello-world_no-hard-coded-passwords]=1 286 | [hello-world_utc]=1 287 | [nats-streaming_no-hard-coded-passwords]=1 288 | [nats-streaming_utc]=1 289 | [nats_no-hard-coded-passwords]=1 290 | [nats_utc]=1 291 | [traefik_no-hard-coded-passwords]=1 292 | [traefik_utc]=1 293 | 294 | # clearlinux has no /etc/passwd 295 | # https://github.com/docker-library/official-images/pull/1721#issuecomment-234128477 296 | [clearlinux_no-hard-coded-passwords]=1 297 | 298 | # alpine/slim/nanoserver openjdk images are headless and so can't do font stuff 299 | [openjdk:alpine_java-uimanager-font]=1 300 | [openjdk:slim_java-uimanager-font]=1 301 | [openjdk:nanoserver_java-uimanager-font]=1 302 | 303 | # the Swift slim images are not expected to be able to run the swift-hello-world test because it involves compiling Swift code. The slim images are for running an already built binary. 304 | # https://github.com/docker-library/official-images/pull/6302#issuecomment-512181863 305 | [swift:slim_swift-hello-world]=1 306 | 307 | # The new tag kernel-slim provides the bare minimum server image for users to build upon to create their application images. 308 | # https://github.com/docker-library/official-images/pull/8993#issuecomment-723328400 309 | [open-liberty:slim_open-liberty-hello-world]=1 310 | 311 | # no "native" dependencies 312 | [ruby:alpine_ruby-bundler]=1 313 | [ruby:alpine_ruby-gems]=1 314 | [ruby:slim_ruby-bundler]=1 315 | [ruby:slim_ruby-gems]=1 316 | 317 | # MySQL-assuming tests cannot be run on MongoDB-providing images 318 | [percona:psmdb_percona-tokudb]=1 319 | [percona:psmdb_percona-rocksdb]=1 320 | 321 | # windows! 322 | [:nanoserver_no-hard-coded-passwords]=1 323 | [:nanoserver_utc]=1 324 | [:windowsservercore_no-hard-coded-passwords]=1 325 | [:windowsservercore_utc]=1 326 | 327 | # https://github.com/docker-library/official-images/pull/2578#issuecomment-274889851 328 | [nats:nanoserver_override-cmd]=1 329 | [nats:windowsservercore_override-cmd]=1 330 | [nats-streaming:nanoserver_override-cmd]=1 331 | [nats-streaming:windowsservercore_override-cmd]=1 332 | 333 | # https://github.com/docker-library/official-images/pull/8329#issuecomment-656383836 334 | [traefik:windowsservercore_override-cmd]=1 335 | 336 | # TODO adjust MongoDB tests to use docker networks instead of links so they can work on Windows (and consider using PowerShell to generate appropriate certificates for TLS tests instead of openssl) 337 | [mongo:nanoserver_mongo-basics]=1 338 | [mongo:nanoserver_mongo-auth-basics]=1 339 | [mongo:nanoserver_mongo-tls-basics]=1 340 | [mongo:nanoserver_mongo-tls-auth]=1 341 | [mongo:windowsservercore_mongo-basics]=1 342 | [mongo:windowsservercore_mongo-auth-basics]=1 343 | [mongo:windowsservercore_mongo-tls-basics]=1 344 | [mongo:windowsservercore_mongo-tls-auth]=1 345 | ) --------------------------------------------------------------------------------