env.php, config.php, app/etc, and Shared Configs in the Release Model
AI generated
CI/CD
.yml
GitLab · Magento · CI/CD · Deployment
env.php, config.php, app/etc
and Shared Configs in the Release Model

env.php does not belong in the repository, and it does not belong in the build artifact. Ignore that rule and you end up with a release model that breaks at the first environment switch. This article explains how app/etc, shared directories, and config.php correctly work together in the symlink release model with GitLab CI/CD.

15 min read env.php · config.php · shared paths · symlinks · GitLab Magento 2.4 · PHP 8.4 · GitLab CI/CD

1. What separates env.php, config.php, and app/etc

Magento has two fundamentally different configuration files in the app/etc/ directory that are frequently confused in practice: env.php and config.php. env.php contains environment specific runtime data: database credentials, Redis connections, the crypt key, the backend URL, and session configuration. This file must never be checked into the Git repository, because it contains secrets that differ for every environment. It is created once on the target server and shared between releases through shared paths.

config.php, on the other hand, contains the module status and can optionally hold store specific configuration exported via bin/magento app:config:dump. This file is far less sensitive and can be versioned in the repository, as long as the team has deliberately decided to manage configuration through the repository. The distinction matters for the release model: anything in the repository is rolled out with every release, while anything in the shared path stays constant across all releases.

The app/etc/ directory itself looks like a simple folder but plays a critical role in the release model. Teams that ship the full directory as part of the build artifact and then copy env.php on top are working correctly. Teams that try to turn app/etc/ itself into a symlink pointing at a shared directory risk Magento's setup system reporting permission errors on the first run. The safest approach is a real folder inside the release directory, with env.php copied into it from the shared area afterward.

2. The release model and the role of shared paths

The symlink release model for Magento works on a simple principle: every release lands in its own directory under releases/. The active release is referenced by a current symlink that the web server uses as its document root. Switching from one release to the next is an atomic operation, a single ln -sfn call, and it takes milliseconds. Requests already in flight against the old release finish normally, while new requests land immediately on the new release.

Shared paths are the bridge between this isolated release concept and the persistent server state. Files and directories that stay identical across releases, or that change independently of the code lifecycle, belong in the shared/ area. The classic candidates in Magento are pub/media/, var/log/, var/session/, and app/etc/env.php. The deploy process creates symlinks to these shared resources for every new release before the current symlink is switched over.

This concept only works if the shared structure is set up before the first deployment and maintained consistently afterward. A common mistake in practice: the first deployment places env.php inside the release directory instead of the shared folder. After a rollback, or on the next deployment, the file is missing from the new release because nobody created the symlink. GitLab pipelines therefore need an explicit step that checks whether the shared folder and its critical files exist before the deploy job proceeds.

3. Setting up the shared directory structure correctly

Setting up the shared structure on the target server is a one time step, but it needs to be documented carefully. Teams that skip documentation run into trouble the moment they set up a second server or migrate to a new one: nobody remembers which directories were created manually or what permissions they need. The shared structure belongs in the infrastructure documentation, ideally as a versioned, reproducible setup script.

For a typical Magento project with multiple stores and a Hyva frontend, the shared structure on the target server looks like this. What matters most is that the var/ folder and its subdirectories are set up as symlinks pointing to a shared path that the web server user can write to. Permission problems in var/ or pub/media/ after a deploy are almost always caused by missing or incorrect symlinks.

# Initial server setup, run once before first deployment
# Script: bin/setup-shared.sh

setup:shared:
  stage: .pre
  when: manual
  script:
    - ssh "$DEPLOY_USER@$DEPLOY_HOST" "bash -s" << 'ENDSSH'
      set -euo pipefail
      BASE="${DEPLOY_PATH}/shared"

      # Create shared directories
      mkdir -p "${BASE}/app/etc"
      mkdir -p "${BASE}/pub/media"
      mkdir -p "${BASE}/var/log"
      mkdir -p "${BASE}/var/session"
      mkdir -p "${BASE}/var/cache"

      # Set correct ownership for web server user
      chown -R www-data:www-data "${BASE}"

      echo "Shared structure ready at ${BASE}"
      ENDSSH

The manual setup job in GitLab is a deliberate choice: it should not run automatically on every deployment, but it should be versioned and reproducible. Keeping it as a manual job in the pipeline guarantees that the shared structure is always set up the same way, regardless of who on the team runs it.

4. Getting env.php onto the server securely

The env.php file contains database passwords, the crypt key, and session configuration. It must never appear in the Git repository or as a GitLab CI artifact. The safest way to get env.php onto the server is a one time manual upload over an encrypted connection during the initial server setup. After that, the file lives permanently in the shared directory and is never overwritten by any deployment process.

Alternatively, env.php can be generated by a scripted step that reads GitLab CI variables. In that case, each individual value is stored as a protected and masked variable in GitLab, and a script assembles the file from those variables. This is more work, but it is fully automatable and avoids manual server access for regular deployments. For teams with several environments this approach is cleaner, because all env.php values are managed centrally in GitLab and environment scopes prevent staging secrets from ending up in production.

Regardless of the approach chosen, the deploy job must verify that env.php exists in the shared directory before switching the current symlink. Deploying to an environment without env.php inevitably leads to a blank screen or a database error in the storefront.

5. config.php in the build artifact or in the shared folder?

Whether config.php lives in the Git repository or in the shared folder has a direct effect on the release model. If config.php lives in the repository, it is rolled out with every deployment. That is the cleaner approach, because changes to the module status are tracked in version control and the build artifact is fully reproducible. Magento teams that make configuration changes via bin/magento config:set, followed by app:config:dump and a Git commit, work under this model.

If config.php lives in the shared folder, it can be changed directly on the server without triggering a new build. That is more convenient for teams that adjust store configuration frequently, but it comes with the downside that the configuration is not version controlled and has to be restored manually after a server rebuild. For production systems the first model is almost always preferable, because it enables auditing, rollback, and reproducibility.

# deploy.yml: shared symlinks and env.php placement
deploy:production:
  stage: deploy
  environment:
    name: production
    url: https://shop.example.com
  script:
    - ssh "$DEPLOY_USER@$DEPLOY_HOST" "bash -s" << ENDSSH
      set -euo pipefail

      RELEASE="${DEPLOY_PATH}/releases/$(date +%Y%m%d-%H%M%S)"
      SHARED="${DEPLOY_PATH}/shared"
      CURRENT="${DEPLOY_PATH}/current"

      # Verify shared structure exists before deploying
      test -f "${SHARED}/app/etc/env.php" \
        || { echo "ERROR: env.php missing in shared"; exit 1; }

      mkdir -p "${RELEASE}"
      rsync -az --delete "${CI_PROJECT_DIR}/" "${RELEASE}/"

      # Link env.php from shared (never from repository)
      cp "${SHARED}/app/etc/env.php" "${RELEASE}/app/etc/env.php"

      # Link persistent directories from shared
      rm -rf "${RELEASE}/pub/media"
      ln -sfn "${SHARED}/pub/media" "${RELEASE}/pub/media"
      rm -rf "${RELEASE}/var/log"
      ln -sfn "${SHARED}/var/log" "${RELEASE}/var/log"
      rm -rf "${RELEASE}/var/session"
      ln -sfn "${SHARED}/var/session" "${RELEASE}/var/session"

      # Atomic switch to new release
      ln -sfn "${RELEASE}" "${CURRENT}"
      echo "Release switched to ${RELEASE}"
      ENDSSH
  only:
    - tags

6. GitLab pipeline: symlinks and shared files in the deploy job

The deploy job in GitLab is responsible for transferring the built artifact to the server and linking the shared paths correctly. This job must be idempotent: it should behave correctly even if it gets run again after a failure. Idempotency means every operation checks its state before making changes. An ln -sfn is already idempotent; a cp overwrites on every run, which is exactly the correct behavior for env.php.

An often overlooked detail: if the deploy job runs inside a Docker container on the GitLab runner, it cannot open an SSH connection with a private key unless that key is made available to the job first. This happens in a before_script section that starts the SSH agent and adds the key from a GitLab CI variable. Teams that forget this step, or implement it carelessly, end up leaving SSH keys behind in a Docker image layer.

For multi server deployments, where several web nodes need to be updated at the same time, the deploy job repeats the symlink operations for every server. The order matters here: only once every node has fully built out the new release directory does the current symlink get switched. That way all nodes run on exactly the same code at exactly the same time.

7. Direct comparison: shared vs. not shared

Deciding which files and directories belong in the shared area, and which ones get rolled out fresh with every release, is one of the most important architecture decisions in the release model. Getting it wrong leads either to data loss or to deployments that need manual fixes on every single run.

File / Directory Shared In Release Artifact Reasoning
app/etc/env.php Yes No Contains secrets, environment specific
app/etc/config.php Possible Preferred Module status is part of the release
pub/media/ Yes No User uploads, release independent
pub/static/ No Yes (build artifact) Rebuilt fresh for every release
var/log/ Yes No Logs span multiple releases

The table draws a clear line: anything that holds secrets, is generated by user interaction, or needs to stay consistent across releases belongs in the shared area. Anything that is produced by the build process and tied to a code version is part of the release artifact. This separation is not just cleaner, it also enables rollbacks that never cause data loss, because persistent data never lives inside the release directory.

8. Common failure patterns with env.php and shared configs

The most common failure pattern in practice is a missing or empty env.php after a deployment. This happens when the deploy job is supposed to copy env.php from the shared directory, but the shared directory does not exist or the file is missing. Magento does not produce a helpful error message in this case; instead you get a blank page or an internal server error. The verify step in the pipeline therefore needs to explicitly confirm that env.php exists and is syntactically valid before the smoke test hits the storefront URL.

A second common failure pattern shows up during the first deployment to a new environment: the developer copies env.php into the release directory instead of the shared directory. The first deployment works fine. The second deployment creates a new release directory that contains no env.php, because it only ever existed in the first release directory. This problem always appears when the shared structure and the deploy process were not set up together.

A third failure pattern involves config.php and the module status. If config.php lives in the shared folder and a deployment activates new modules without updating the shared config.php, Magento starts after the deployment with an inconsistent module status. This typically shows up as missing layout blocks, errors in the admin panel, or extensions that simply do not work. The fix is either to consistently version config.php in the repository, or to implement a post deploy step that runs bin/magento app:config:import.

9. Rollback and shared files: what changes, what stays

The key advantage of the symlink release model is that rolling back to an earlier release destroys no data. Because all persistent data lives in the shared area and not in the release directory, that data fits the old release just as well as it fits the new one. A rollback is semantically identical to switching the current symlink to an older release directory, followed by a cache flush.

The one exception is database migrations. If a new release includes database schema changes applied through bin/magento setup:upgrade, the database may no longer be fully compatible with the old code after a rollback. For that case, the rollback plan needs to explicitly address database compatibility, either through backward compatible schema changes (the expand contract pattern) or through a database backup taken before the deployment.

# rollback.yml: rollback to a specific previous release
rollback:production:
  stage: rollback
  when: manual
  environment:
    name: production
  script:
    - ssh "$DEPLOY_USER@$DEPLOY_HOST" "bash -s" << ENDSSH
      set -euo pipefail

      # List available releases for manual selection
      echo "Available releases:"
      ls -lt "${DEPLOY_PATH}/releases/" | head -10

      # PREVIOUS_RELEASE must be passed as variable in manual trigger
      TARGET="${DEPLOY_PATH}/releases/${ROLLBACK_TO}"
      test -d "${TARGET}" || { echo "ERROR: Release not found: ${TARGET}"; exit 1; }

      # Shared files remain unchanged, only the symlink switches
      ln -sfn "${TARGET}" "${DEPLOY_PATH}/current"

      cd "${DEPLOY_PATH}/current"
      bin/magento cache:flush
      echo "Rolled back to ${TARGET}"
      ENDSSH
  variables:
    ROLLBACK_TO: ""  # Set via GitLab manual job trigger

10. Summary

Handling env.php, config.php, and shared directories in the release model correctly is not an optional refinement, it is the foundation for stable Magento deployments. env.php belongs exclusively in the shared area and must never show up in the repository or in the build artifact. config.php should preferably be versioned in the repository, so that the module status is part of the release specific artifact. Directories such as pub/media/, var/log/, and var/session/ need to be linked to the shared area with symlinks before the current symlink is switched.

The deploy job in GitLab CI needs to run these steps in the right order and verify that all shared files exist before switching the current symlink. A verify job after the deployment makes sure env.php is wired up correctly and the storefront is reachable. Rollbacks work in the symlink model without data loss, as long as database compatibility is accounted for.

env.php and Shared Configs in the Release Model: the essentials at a glance

env.php

Lives in the shared folder, never versioned. Deploy job checks it exists before the symlink switch. Generating it from GitLab variables is the cleanest solution.

config.php

Preferably versioned in the repository: module status is part of the release. Post deploy app:config:import keeps everything consistent.

Shared structure

pub/media, var/log, var/session as symlinks to shared paths. One time setup, versioned setup script, fully reproducible.

Rollback

Switch the symlink to an old release, flush the cache. Shared data stays untouched. Plan database compatibility separately.

11. FAQ: env.php, config.php, and Shared Configs in the Release Model

1Can env.php go into the Git repository?
No. env.php contains secrets and is environment specific. It belongs exclusively in the shared directory on the target server.
2Difference between env.php and config.php?
env.php contains runtime secrets. config.php contains the module status and can be versioned in the repository.
3How is env.php brought to the server securely?
A one time manual upload during server setup, or generation from GitLab protected and masked variables in the deploy script.
4What happens to shared files on rollback?
Shared files stay untouched. Only the current symlink switches to an older release directory. No data loss.
5app/etc/ as a symlink to shared?
Not recommended. A safer approach is a real folder inside the release with env.php copied in from the shared area.
6When does config.php become a problem?
When it lives in the shared folder and new modules get activated without updating the shared file, resulting in an inconsistent module status.
7How does the verify job check env.php?
php -r 'require "app/etc/env.php";' on the server checks existence and syntax. bin/magento cache:status validates the database connection.
8How many old releases should be kept?
Two to three for a quick rollback. More than five to seven ties up storage unnecessarily. Automate the cleanup job.
9Does pub/static/ belong in the shared area?
No. pub/static/ is rebuilt fresh for every release and is part of the release artifact, not the shared area.
10Risk of an unversioned shared structure?
When rebuilding a server, nobody knows which directories were created with which permissions. The setup script must be versioned.