failed to create symbolic link 'public/storage/public': File exists

Stefan Bogdanescu

Founder & Senior Architect · 2026-06-29

Laravel Company

Fixing the Symbolic Link Nightmare: Solving 'File Exists' in Composer CI/CD

As developers working with Continuous Integration/Continuous Deployment (CI/CD) pipelines, we often encounter frustrating errors that seem trivial but halt the entire build process. One such common issue arises when running dependency management tools like Composer in automated environments. Today, we are diving deep into a specific error: failed to create symbolic link 'public/storage/public': File exists, which frequently pops up during a composer install executed via .gitlab-ci.yml.

This post will break down why this happens and provide robust, developer-focused solutions to ensure your build scripts run smoothly, especially when managing file system operations within frameworks like Laravel.


Understanding the Error: Why Symbolic Links Fail

The specific error you are seeing—ln: failed to create symbolic link 'public/storage/public': File exists—is a standard operating system error. It means the ln (link) command attempted to create a symbolic link pointing from storage/app/public to public/storage, but it failed because a file or link with the exact destination path (public/storage/public) already exists.

In the context of Composer's post-install-cmd script, this operation is usually performed to create a necessary symbolic link that connects the application's storage directory to the public web accessible directory. While this step is crucial for Laravel applications (where you often link storage/app/public to public/storage), the failure indicates a conflict in the execution environment rather than an error in the code itself.

Root Causes of the Failure in CI/CD

This issue rarely stems from faulty Composer dependencies. Instead, it is almost always related to one of the following root causes common in automated environments:

  1. Idempotency Failure: The script assumes a link needs to be created, but because the previous build step (or another part of the pipeline) already ran successfully, the required file system structure is already in place. The script lacks logic to check for existence before attempting creation.
  2. Artifact Contamination: In CI/CD systems like GitLab, artifacts or cached files from previous failed runs might persist in the working directory, leading to conflicting states when a new run attempts to execute the same setup script.
  3. Race Conditions: Although less common with Composer’s hooks, race conditions can occur if multiple processes attempt file system operations simultaneously.

The Solution: Implementing Idempotent Scripting

The best way to fix this is not to fight the error, but to make your scripts idempotent—meaning running the script multiple times yields the same result without causing errors. We need to modify the script that Composer executes to safely handle existing files.

Since the error occurs within a custom post-install-cmd script (which is often set up by framework conventions), we should wrap the command with checks.

Method 1: Conditional Linking (The Robust Fix)

Instead of blindly running ln -sr, we check if the target link already exists before attempting to create it. This ensures that subsequent runs are safe and don't throw errors.

Modify your post-install script (or the command executed by Composer) to include conditional logic:

#!/bin/sh

# Define source and target paths
SOURCE_DIR="storage/app/public"
TARGET_LINK="public/storage"

# Check if the link already exists before attempting creation
if [ ! -L "$TARGET_LINK" ]; then
    echo "Creating symbolic link: $TARGET_LINK pointing to $SOURCE_DIR"
    ln -sr "$SOURCE_DIR" "$TARGET_LINK"
else
    echo "Symbolic link $TARGET_LINK already exists. Skipping creation."
fi

Why this works: This logic explicitly checks if the target symbolic link (public/storage) is missing (! -L). If it is present, the script gracefully skips the ln command, preventing the "File exists" error entirely.

Method 2: Pre-cleanup in CI Jobs

If you are dealing with persistent artifact issues (Root Cause #2), ensure that your CI job performs a clean slate before running dependency installation. Add a step to wipe temporary directories or rebuild dependencies if necessary:

# Example snippet for .gitlab-ci.yml
install_dependencies:
  stage: build
  script:
    - rm -rf vendor/
    - composer install --no-dev --optimize-autoloader
    # Followed by your custom post-install script execution

Conclusion

Dealing with file system operations in automated environments requires a shift from simple command execution to robust, defensive scripting. By adopting idempotent practices—checking for the existence of files and links before performing write operations—you can eliminate frustrating errors like File exists during Composer installations. This approach is fundamental to building reliable and scalable pipelines, whether you are deploying applications built on frameworks like Laravel or any other modern stack. For deeper dives into framework best practices that ensure clean deployments, always refer to established resources like those found at https://laravelcompany.com.