Deploying a secure Java Application Docker Image using Github

Shaily Aggarwal
5 min readMay 12, 2022

--

I was looking for a way to use Github Actions to deploy my Java application as a docker image more securely, with zero cost in the process (like vault or image repository cost). That’s when I figured out a way to leverage Github secrets and Docker Hub combination.

The entire codebase is available on Github (https://github.com/ShailyAggarwalK/gh-actions-docker)

Pre-Requisites

  1. Github Account
  2. DockerHub Account (please generate a docker hub access token to be able to push image, copy it’s value as soon as you generate as it’s not accessible later.)
  3. Basic knowledge of pulling docker image and starting docker container with 8080 port exposed.

Challenge

  1. Github Secrets can only be accessed in workflow-action.yaml file or the build pipeline.
  2. Also, if we try to access them in application using docker environment variable, secret credentials gets exposed or printed either in docker image logs or github logs.
  3. I wanted a way to access those in my application code (in this example — application.properties), such that they are not exposed anywhere in build process.

How I solved the problem :

Deployment Flow

I have divided the Solution into 5 simple steps listed below

Solution Steps

Step 1: Create with basic Java Spring Boot Starter Application
Since we usually take passwords from application.properties (in local Setup), I am just printing the secret application properties using actuator.

build.gradle (I am using gradle for dependency management and app setup)

// build.gradle
plugins {
id 'org.springframework.boot' version '2.6.2'
id 'io.spring.dependency-management' version '1.0.11.RELEASE'
id 'java'
}

group = 'com.gh-actions-demo'
sourceCompatibility = '11'

repositories {
mavenCentral()
}

dependencies {
implementation 'org.springframework.boot:spring-boot-starter'
implementation 'org.springframework.boot:spring-boot-starter-actuator'
implementation 'org.springframework.boot:spring-boot-starter-web'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
}

test {
useJUnitPlatform()
}

Application.java : (just to run actuator to display app properties)

//Application.java
package
com.ghactionsdemo.ghactionsdocker;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.web.bind.annotation.RestController;

@SpringBootApplication
@RestController
public class
GhActionsDockerApplication {
public static void main(String[] args) {
SpringApplication.run(GhActionsDockerApplication.class, args);
}

application.properties (set up environment variables)

// application.properties
management.endpoints.web.exposure.include
=env,health
SOME_ENV=test

Step 2: Enable Github Actions for Application
Create a .github/workflows repository and add action.yaml (Initial Github workflow file)
Initially, it just has one step, which checks out from repo on every push but there is no target for code to be deployed yet. We will add it in next steps.

// action.yaml
name
: CI

on:
push:
branches: [ main ]
pull_request:
branches: [ main ]
workflow_dispatch:

jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v2

Step 3: Dockerize the Application to be stored on DockerHub
Let’s create a shippable docker image. This also consumes docker username and password from github secrets but only in action.yaml.

  1. Run ./gradlew clean build` to generate gh-actions-docker.jar in build folder. Copy and paste this jar file in project root so that we can use it in DockerFile below.
  2. Add DockerFile :
// DockerFile
FROM
openjdk

ADD gh-actions-docker.jar /
ENTRYPOINT
["java", "-jar","/gh-actions-docker.jar"]
EXPOSE 8080

3. On Github UI, go to repository settings and Create Github Secrets : DOCKER_USERNAME and DOCKER_TOKEN

4. Add the following steps to .github/workflows/action.yaml in build stage itself :
Below, we access the github secrets value using `${{secrets.<SECRET_NAME>}}`

action.yaml

### action.yaml
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v1
- name: Login to Docker Hub
uses: docker/login-action@v1
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_TOKEN }}
- name: Build and push
uses: docker/build-push-action@v2
with:
context: .
push: true
tags: ${{ secrets.DOCKER_USERNAME }}/gh-actions-docker:latest

This will create docker image in docker hub on every push, which can be pulled and application started using container (make sure to expose application port).
Once the container is up, open http://localhost:8080/actuator/env in browser, it lists down all environment variables spring has access to. (lookout for SOME_ENV value there)
Notice, that DOCKER_USERNAME and DOCKER_PASSWORD are not present at this link, as they are not accessible to spring application.

For security reasons, we will not expose docker credentials to application, but will create different secrets for this exercise (we usually need to secure other credentials like database, external api credentials which spring application then uses at run-time)

Step 4: Create Github Secrets for Application level credentials

  1. Create 2 more Github secrets: DB_USERNAME and DB_PASSWORD

Step 5: Add newly created Github Secrets to docker image

Since, we already have a static application.properties, we will create another application.properties in config directory with DB credentials, so that all application.properties are picked by application.

  1. Update action.yaml
    - First we create config directory
    - Then, we dynamically create application.properties file using the plugin https://github.com/marketplace/actions/create-env-file
    - this python plugin is responsible for reading github secrets and writing it to a env file with key provided (prefix envkey_ mandatory)

action.yaml

### action.yaml
- name: make config directory
run: |
mkdir config

- name: Make envfile
uses: SpicyPizza/create-envfile@v1.3
with:
envkey_DBUSERNAME: ${{ secrets.DB_USERNAME }}
envkey_DBPASSWORD: ${{ secrets.DB_PASSWORD }}
file_name: application.properties
directory: ./config/

- name: Test env file
run: |
echo "$(cat config/application.properties)"

2. Update DockerFile to include config directory :

DockerFile

// DockerFile
FROM
openjdk
ADD config /config
ADD gh-actions-docker.jar /
ENTRYPOINT ["java", "-jar","/gh-actions-docker.jar"]
EXPOSE 8080

Also, build can give following warning in Github :

expected warning in gitHub actions

we can ignore this warning as it’s a known issue in create-env-file plugin

Once above changes are pushed and build succeeds, new image should be created in DockerHub.

Step 6: Add newly created Github Secrets to docker image

Docker pull the new image and run it with 8080 port exposed.
We can view the DB_USERNAME and DB_PASSWORD (masked) at required link : http://localhost:8080/actuator/env

sample output

Last But Not The Least

We also notice that using this approach doesn’t print or expose the secret values anywhere else (image build logs in docker-hub, github build logs) except in application container when pulled on local. This Docker image can be made private to ensure safe pulls.

Say Hi 👋 on LinkedIn or Twitter

--

--