Baeldung Pro – Ops – NPI EA (cat = Baeldung on Ops)
announcement - icon

Learn through the super-clean Baeldung Pro experience:

>> Membership and Baeldung Pro.

No ads, dark-mode and 6 months free of IntelliJ Idea Ultimate to start with.

1. Introduction

When building applications, we often need to use and manage configuration settings, API keys, database URLs, and other sensitive information. These are typically stored in environment files, like .env files, that an application reads at runtime. Yet, how can this same mechanism be used when we want to deploy or test an application using GitHub Actions? We need a way to create such environment files dynamically during workflow execution.

In this tutorial, we’ll explore using GitHub Actions to create an .env file in the workflow. First, we’ll cover what .env files are. After that, we’ll discuss creating a simple .env file, as well as using it at a later point within a given workflow. Subsequently, we’ll elaborate on creating multiple .env files. Lastly, we’ll consider related security practices.

2. Environment Files

Although they typically employ the .env extension and no filename, environment files are simple text files that contain key-value pairs representing configuration variables for an application:

DATABASE_URL=postgresql://localhost:5432/myapp
API_KEY=secret-api-key
NODE_ENV=production
PORT=3000

Applications read these variables at startup, enabling us to configure different settings for development, testing, and production environments without hardcoding values in the source code. These are subsequently available in the code environment.

Thus, secret or dynamic data can vary across deployments.

3. Creating .env Files

When code runs in GitHub Actions workflows, it doesn’t have access to the .env files on any local machine.

Because of this, we might need to recreate .env files during the workflow execution for several reasons:

  • An application might require certain environment variables to compile, run tests, or start properly.
  • When deploying to production or staging environments, we need to provide the correct configuration values for that specific environment.
  • Rather than committing sensitive information, such as API keys, to a repository, we can store them as GitHub Secrets and inject them into environment files during workflow execution.

In fact, without some variables, the application and, by extension, the whole workflow could fail. Therefore, it’s essential to learn how to create .env files within workflows.

3.1. Creating a Simple .env File

Usually, the simplest way to create an environment file in GitHub Actions is to use the echo command to write content to a file.

However, for this tutorial, we create the file using GitHub Actions from GitHub. Let’s look at the YAML sample workflow env-file.yml:

name: Build and Test
on: 
  - push
  - pull_request
jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout code
        uses: actions/checkout@v3
      - name: Create .env file
        run: |
          echo "NODE_ENV=production" >> .env
          echo "PORT=3000" >> .env
          echo "DATABASE_URL=postgresql://localhost:5432/testdb" >> .env
      - name: Install dependencies
        run: npm install
      - name: Run tests
        run: npm test

The above code is a GitHub Actions workflow, titled Build and Test. It triggers when developers push code or open a pull_request. It defines a single job called build, which runs on the latest Ubuntu environment. First, the workflow checks out the repository using the actions/checkout@v3 action, making the code available for the subsequent steps. Then, it creates an .env file by appending environment variables, so the application can access runtime configuration settings.

Since this is a NodeJS project, after setting up the environment file, the workflow installs the project dependencies via npm install. This ensures that all necessary packages listed in package.json are available before testing. Finally, the workflow runs the project test suite using the npm test command.

Notably, this approach works well for non-sensitive configuration values that we’re comfortable having visible in the workflow logs. However, not all data can be shared this way.

3.2. Working with GitHub Secrets

For sensitive information like API keys, passwords, or tokens, we use GitHub Secrets. GitHub Secrets are encrypted values stored securely in the repository settings that can be accessed in workflows without exposing their actual values.

To set up a GitHub secret, we follow a few steps:

  1. Go to the respective repository
  2. Click the Settings tab
  3. Choose Secrets and variables

Let’s see an example screen:

Secrets and variables in GitHub

From the drop-down menu, we click Actions and then go to Manage environment secrets under Environment secrets:

Actions, secrets and variables in GitHub

Lastly, we add the required secret and save it. Once we set up the secrets, we can reference them in the workflow.

Let’s modify the .yml file from earlier to include those secrets:

name: Deploy Application
on:
  push:
    branches: [main]
jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      - name: Create .env file
        run: |
          echo "NODE_ENV=production" >> .env
          echo "DATABASE_URL=${{ secrets.DATABASE_URL }}" >> .env
          echo "API_KEY=${{ secrets.API_KEY }}" >> .env
          echo "JWT_SECRET=${{ secrets.JWT_SECRET }}" >> .env
      - name: Build application
        run: npm run build
      - name: Deploy to server
        run: npm run deploy

The ${{ secrets.SECRET_NAME }} syntax enables us to access the stored secrets safely. GitHub automatically masks these values in workflow logs, so these won’t be visible to anyone viewing the workflow output.

3.3. Using the .env File Later in the Workflow

Once the .env file is created, we can use it in any future steps by sourcing it:

- name: Run app with env vars
  run: |
    source .env
    echo "Running app with API key: $API_KEY"

In most tools, just having the .env file in the root folder is enough. We don’t have to create it in other subdirectories.

4. Advanced Techniques

In this section, we discuss advanced use cases of .env files within GitHub Actions.

4.1. Creating Multiple Environment Files

Sometimes we need different environment files for different stages of the workflow.

In such instances, we can use GitHub Actions to create separate environment files:

- name: Create test environment file
  run: |
    echo "NODE_ENV=test" >> .env.test
    echo "DATABASE_URL=${{ secrets.TEST_DATABASE_URL }}" >> .env.test
    echo "API_KEY=${{ secrets.TEST_API_KEY }}" >> .env.test

- name: Create production environment file
  run: |
    echo "NODE_ENV=production" >> .env.production
    echo "DATABASE_URL=${{ secrets.PROD_DATABASE_URL }}" >> .env.production
    echo "API_KEY=${{ secrets.PROD_API_KEY }}" >> .env.production

Each step above creates a different .env file containing secrets injected from the GitHub environment, enabling different configurations for test and production stages.

This approach keeps the deployment process clean and secure across environments.

4.2. Using Conditional Logic

Moreover, we can create different environment configurations based on the branch or event that triggered the workflow:

- name: Create environment file
  run: |
    if [ "${{ github.ref }}" == "refs/heads/main" ]; then
      echo "NODE_ENV=production" >> .env
      echo "DATABASE_URL=${{ secrets.PROD_DATABASE_URL }}" >> .env
    else
      echo "NODE_ENV=development" >> .env
      echo "DATABASE_URL=${{ secrets.DEV_DATABASE_URL }}" >> .env
    fi

For instance, the above snippet checks the current branch name and writes the appropriate environment settings into the .env file.

This method is useful for automatically switching between production and development configurations.

5. Best Practices and Security Considerations

In GitHub Actions workflows, maintaining clean and secure handling of secrets is essential. Real secrets should never be committed to the repository. However, we can use placeholder values in any committed .env files. In addition, secret names should be clear and descriptive. Rather than labels like SECRET1, names such as DATABASE_PASSWORD or STRIPE_API_KEY provide better clarity and organization.

Moreover, environment-specific secrets, such as those for development, staging, or production, help reduce potential impact. In the case of a compromised secret, isolation between environments limits the effect. Furthermore, periodic rotation of secrets is considered a good security measure. Especially in production environments, regular updates to keys or tokens serve as an added layer of protection.

We should avoid inserting unused variables in larger files. On the contrary, it’s a good measure to include only relevant secrets in the .env file.

Furthermore, if variables fail to load, we might also need to check where the application expects the .env file. Also, a frequent cause of missing secrets is a mismatch in secret names. Because GitHub Secrets are case-sensitive, secret names must exactly match those set in the repository settings.

Finally, sensitive values and full .env files shouldn’t be printed in production logs. Using commands to print out .env temporarily during debugging is fine for development, but we must remove any such statements once verification is complete.

6. Conclusion

In this article, we covered using GitHub Actions to create and work with an .env file in the workflow.

Creating environment files in GitHub Actions is an important skill for managing configuration and secrets in the CI/CD pipelines. By combining basic file creation techniques with GitHub Secrets, we can securely manage sensitive information while keeping the workflows flexible and maintainable.