Merter Gülbahar - 💻 Kişisel Web Bloğu!

Docker Build ile Bir Angular Projesini Tek Docker Image'a Dönüştürüp Çoklu Ortamda Kullanma

Bir angular projesinin docker ile image’ını bir kere oluşturup, birden fazla ortamda nasıl çalıştırabileceğimizi göreceğiz.

1. Adım

Environment Dosyaları değiştirme

Angular projenizdeki src/assets/ yoluna gidelim. Burada environments isimli bir klasör oluşturalım. Bu klasörün içine env.json, env.prod.json, env.test.json isimlerinde json dosyalarını oluşturalım.

env.json dosyasının içine config’leri yazın. Örneğin;


{
  "production": false,
  "isLogConsole": false,
  "apiUrl": "http://localhost:4200",
  "tokenRefreshInterval": 3600
}

env.prod.json dosyasının içine config’lerimizi yazalım. Örneğin;


{
  "production": true,
  "isLogConsole": true,
  "apiUrl": "https://api.prod.com",
  "tokenRefreshInterval": 3600
}

env.test.json dosyasına da benzer şekilde config’lerinizi yazalım.

Config Dosyasının (env.json) Kullanımı

config-model.ts isminde bir dosya oluşturalım. Bu dosya env.json dosyasındaki verileri okumayı kolaylaştırmak içindir.

export type ConfigModel = {
  production: boolean;
  isLogConsole: true;
  apiUrl: string;
  tokenRefreshInterval: number;
};

configuration-loader.ts isminde bir dosya oluşturalım. Dosya içeriği aşağıdaki gibidir. Aşağıdaki kodlarla env.json dosyasındaki config’ler elde edilir.

import {Injectable} from '@angular/core';
import { HttpClient } from '@angular/common/http';
import {ConfigModel} from "../model/config-model";

@Injectable({
  providedIn: "root"
})

export class ConfigurationLoader {
  private readonly CONFIG_FILE_URL = "./assets/environments/env.json";
  private config: ConfigModel;

  constructor(private http: HttpClient) {}

  public loadConfiguration(): Promise<ConfigModel> {
    return this.http
      .get(this.CONFIG_FILE_URL)
      .toPromise()
      .then((config: any) => {
        this.config = config;
        return config;
      })
      .catch((error: any) => {
        console.error(error);
      });
  }

  getConfiguration(): ConfigModel {
    return this.config;
  }
}

Daha sonra app.module.ts isimli dosyayı bulalım. Aşağıdaki kodları bu dosyaya ekleyelim. Modül başlatılırken env.json dosyası okunacaktır ve config’ler hazır hale gelecektir.

import {ConfigurationLoader} from "./service/configuration-loader";
import {ConfigModel} from "./model/config-model";

...
export function loadConfiguration(configService: ConfigurationLoader): () => Promise<ConfigModel> {
  return () => configService.loadConfiguration();
}

...

@NgModule({
  ...
   providers: [
    ...
    {
      provide: APP_INITIALIZER,
      useFactory: loadConfiguration,
      deps: [ConfigurationLoader],
      multi: true
    }
  ]
  ...
})
...

base-service.ts isimli bir servisimiz olsun. Bu servisin içine config’lerin okunması için aşağı kodu ekleyelim.


@Injectable()
export class BaseService {
  environment: ConfigModel;
  constructor(private configLoader: ConfigurationLoader) {
    this.environment = configLoader.getConfiguration();
  }

  getList(apiControllerName: string, action: string){
    return this.http.get(this.environment.apiUrl + '/' + apiControllerName + '/' + action, ...);
  }
}

2. Adım

angular.json dosyasını açalım. build‘in altındaki configurations kısmını aşağıdaki şekilde güncelleyelim.

...
"build": {
  ...
  "configurations": {
    "prod": {
      "fileReplacements": [
        {
          "replace": "src/assets/environments/env.json",
          "with": "src/assets/environments/env.prod.json"
        }
      ],
      ...
    },
    "test": {
      "fileReplacements": [
        {
          "replace": "src/assets/environments/env.json",
          "with": "src/assets/environments/env.test.json"
        }
      ],
      ...
    }
  }
  ...
}
...

3. Adım

package.json dosyasını açalım. Build için bir config tanımlayalım. test veya prod olarak environment kullanacak olsak dahi prod için build alabiliriz. Config dosyasını proje başlatılırken zaten değiştireceğiz. Burada amaç angular.json ‘da prod için belirtilen diğer configleri projemize uygulamak. angular.json dosyasına test ve prod için ortak bir config de yazılabilirdik. Daha sonra o config’i build alırken parametre olarak geçebilirdik.

...
"scripts":{
  ...
  "build_prod": "ng build --configuration prod --outputPath=dist/angular --namedChunks=false --aot --output-hashing=all --sourceMap=false",
  ...
}
...

4. Adım

main.ts dosyasını açın ve aşağıdaki şekilde güncelleyelim. Proje başlatılırken docker run ile geçeceğimiz test yada prod environment’ına göre bu dosyalardan birinin içeriğini env.json dosyasına kopyalanacak ve dosya içeriği okunacaktır.


fetch('./assets/environments/env.json').then(response => {
  return response.json();
}).then(data => {
  if (data.production) {
    enableProdMode();
  }
  platformBrowserDynamic().bootstrapModule(AppModule)
    .catch(err => console.error(err));
});

Angular tarafında işimiz bitti.

5. Adım

Angular projesindeki ana dizinde (Dockerfile’ın bulunduğu) entrypoint.sh isminde bir dosya oluşturalım. Bu dosyanın içine aşağıdaki komutları ekleyelim.

cp /usr/share/nginx/html/assets/environments/env.${ENVIRONMENT}.json /usr/share/nginx/html/assets/environments/env.json
/usr/sbin/nginx -g "daemon off;"

Daha sonra Dockerfile’ı açalım. Ve aşağıdaki kodları dosyamıza ekleyelim.

FROM node:10-alpine as build-step
RUN mkdir -p /app
WORKDIR /app
COPY package.json /app
RUN npm install
COPY . /app
RUN npm run build_prod

FROM nginx:stable
COPY nginx.conf /etc/nginx/conf.d/default.conf
COPY --from=build-step /app/dist/angular /usr/share/nginx/html
COPY ["entrypoint.sh", "/entrypoint.sh"]
RUN chmod +x /entrypoint.sh
RUN chmod -R 755 /usr/share/nginx/html/
RUN chown -R nginx:nginx /usr/share/nginx/html/

CMD ["sh", "/entrypoint.sh"]

6. Adım

Docker image’ın çalıştırılması

Image Oluşturma

$ docker image -t deneme-image:latest .

Container’ı Ayağa Kaldırma

$ docker run -dit  -e ENVIRONMENT=test -e TZ=Europe/Istanbul -p 80:80 --name=cont_test --restart=always deneme-image:latest
$ docker run -dit  -e ENVIRONMENT=prod -e TZ=Europe/Istanbul -p 80:80 --name=cont_prod --restart=always deneme-image:latest

Kaynaklar:

https://itnext.io/how-does-app-initializer-work-so-what-do-you-need-to-know-about-dynamic-configuration-in-angular-718e7c345971 https://gabrielemilan.dev/publish-same-docker-image-with-angular-application-to-any-environments-edbfc454c6bb