Skip to content

Commit

Permalink
feat: add distroless image
Browse files Browse the repository at this point in the history
Distroless images are also available with `*-distroless` tags; for example, v1 is available as
`mattwebbio/orbital-sync:1-distroless`. These images are smaller and more secure than the
default Alpine-based images, because they contain only the Orbital Sync code and its direct
dependencies. They do not include a shell, package manager, or other tools that are typically
present in a Linux distribution. These will be the default images in a future major version
release.
  • Loading branch information
mattwebbio committed Mar 25, 2024
1 parent 86b957c commit a93c38e
Show file tree
Hide file tree
Showing 6 changed files with 169 additions and 49 deletions.
42 changes: 37 additions & 5 deletions .github/workflows/perform-release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,8 @@ jobs:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Generate tags
id: docker_tags
- name: Generate tags (alpine)
id: alpine_docker_tags
uses: docker/metadata-action@v5
with:
images: |
Expand All @@ -47,13 +47,45 @@ jobs:
type=semver,pattern={{major}}.{{minor}}
type=semver,pattern={{major}}
type=sha
- name: Build and push
- name: Build and push (alpine)
uses: docker/build-push-action@v5
with:
context: .
build-args: |
BASE_IMAGE=node:18-alpine
push: true
tags: ${{ steps.docker_tags.outputs.tags }}
labels: ${{ steps.docker_tags.outputs.labels }}
tags: ${{ steps.alpine_docker_tags.outputs.tags }}
labels: ${{ steps.alpine_docker_tags.outputs.labels }}
platforms: |
linux/amd64
linux/arm64
linux/arm
- name: Generate tags (distroless)
id: distroless_docker_tags
uses: docker/metadata-action@v5
with:
images: |
mattwebbio/orbital-sync
ghcr.io/mattwebbio/orbital-sync
tags: |
type=schedule
type=ref,event=branch
type=ref,event=pr
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
type=semver,pattern={{major}}
type=sha
flavor: |
suffix=-distroless,onlatest=true
- name: Build and push (distroless)
uses: docker/build-push-action@v5
with:
context: .
build-args: |
BASE_IMAGE=gcr.io/distroless/nodejs18:latest
push: true
tags: ${{ steps.distroless_docker_tags.outputs.tags }}
labels: ${{ steps.distroless_docker_tags.outputs.labels }}
platforms: |
linux/amd64
linux/arm64
Expand Down
37 changes: 32 additions & 5 deletions .github/workflows/prerelease.yml
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,8 @@ jobs:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Generate tags
id: docker_tags
- name: Generate tags (alpine)
id: alpine_docker_tags
uses: docker/metadata-action@v5
with:
images: |
Expand All @@ -41,13 +41,40 @@ jobs:
type=ref,event=branch
type=ref,event=pr
type=sha
- name: Build and push
- name: Build and push (alpine)
uses: docker/build-push-action@v5
with:
context: .
build-args: |
BASE_IMAGE=node:18-alpine
push: true
tags: ${{ steps.docker_tags.outputs.tags }}
labels: ${{ steps.docker_tags.outputs.labels }}
tags: ${{ steps.alpine_docker_tags.outputs.tags }}
labels: ${{ steps.alpine_docker_tags.outputs.labels }}
platforms: |
linux/amd64
linux/arm64
linux/arm
- name: Generate tags (distroless)
id: distroless_docker_tags
uses: docker/metadata-action@v5
with:
images: |
ghcr.io/mattwebbio/orbital-sync
tags: |
type=ref,event=branch
type=ref,event=pr
type=sha
flavor: |
suffix=-distroless,onlatest=true
- name: Build and push (distroless)
uses: docker/build-push-action@v5
with:
context: .
build-args: |
BASE_IMAGE=gcr.io/distroless/nodejs18:latest
push: true
tags: ${{ steps.distroless_docker_tags.outputs.tags }}
labels: ${{ steps.distroless_docker_tags.outputs.labels }}
platforms: |
linux/amd64
linux/arm64
Expand Down
9 changes: 7 additions & 2 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
ARG BASE_IMAGE


FROM node:18-alpine as builder
ENV NODE_ENV=development

Expand All @@ -15,7 +18,7 @@ COPY . .
RUN yarn install --production


FROM node:18-alpine
FROM ${BASE_IMAGE}
ENV NODE_ENV=production

WORKDIR /usr/src/app
Expand All @@ -24,4 +27,6 @@ COPY package.json yarn.lock ./
COPY --from=builder /usr/src/app/dist ./dist
COPY --from=install /usr/src/app/node_modules ./node_modules

CMD [ "node", "dist/index.js" ]
ENV PATH=$PATH:/nodejs/bin
ENTRYPOINT [ "node" ]
CMD [ "dist/index.js" ]
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,8 @@ The Orbital Sync Docker image is published to both DockerHub and the GitHub Pack
[mattwebbio/orbital-sync](https://hub.docker.com/r/mattwebbio/orbital-sync)<br />
[ghcr.io/mattwebbio/orbital-sync](https://github.com/mattwebbio/orbital-sync/pkgs/container/orbital-sync)
[Distroless images](https://github.com/GoogleContainerTools/distroless/blob/main/README.md) are also available with `*-distroless` tags; for example, v1 is available as `mattwebbio/orbital-sync:1-distroless`. These images are smaller and more secure than the default Alpine-based images, because they contain only the Orbital Sync code and its direct dependencies. They do not include a shell, package manager, or other tools that are typically present in a Linux distribution. These will be the default images in a future major version release.

### Node

[![NPM Downloads](https://img.shields.io/npm/dt/orbital-sync?logo=npm&style=for-the-badge)](https://www.npmjs.com/package/orbital-sync)
Expand Down
15 changes: 13 additions & 2 deletions test/containers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,17 @@ export function createPiholeContainer({
.withWaitStrategy(Wait.forHealthCheck());
}

export function createOrbitalSyncContainer(): Promise<GenericContainer> {
return GenericContainer.fromDockerfile('./', 'Dockerfile').build();
export function createOrbitalSyncContainer(
baseImage: OrbitalBaseImage = OrbitalBaseImage.Alpine
): Promise<GenericContainer> {
return GenericContainer.fromDockerfile('./', 'Dockerfile')
.withBuildArgs({
BASE_IMAGE: baseImage
})
.build();
}

export enum OrbitalBaseImage {
Alpine = 'node:18-alpine',
Distroless = 'gcr.io/distroless/nodejs18:latest'
}
113 changes: 78 additions & 35 deletions test/e2e/two-targets.test.ts
Original file line number Diff line number Diff line change
@@ -1,41 +1,84 @@
import { Network } from 'testcontainers';
import { createOrbitalSyncContainer, createPiholeContainer } from '../containers';
import {
OrbitalBaseImage,
createOrbitalSyncContainer,
createPiholeContainer
} from '../containers';
import { inspectById } from '../docker';
import sleep from 'sleep-promise';

describe('Orbital', () => {
it('should sync two targets and exit with "zero" exit code', async () => {
const network = await new Network().start();
const [pihole1, pihole2, pihole3, orbitalImage] = await Promise.all([
createPiholeContainer({ password: 'primary' }).withNetwork(network).start(),
createPiholeContainer({ password: 'secondary' }).withNetwork(network).start(),
createPiholeContainer({ password: 'tertiary' }).withNetwork(network).start(),
createOrbitalSyncContainer()
]);

const orbital = await orbitalImage
.withEnvironment({
PRIMARY_HOST_BASE_URL: `http://${pihole1.getIpAddress(network.getName())}`,
PRIMARY_HOST_PASSWORD: 'primary',
SECONDARY_HOST_1_BASE_URL: `http://${pihole2.getIpAddress(network.getName())}`,
SECONDARY_HOST_1_PASSWORD: 'secondary',
SECONDARY_HOST_2_BASE_URL: `http://${pihole3.getIpAddress(network.getName())}`,
SECONDARY_HOST_2_PASSWORD: 'tertiary',
RUN_ONCE: 'true',
VERBOSE: 'true'
})
.withLogConsumer((stream) => stream.on('data', (chunk) => console.log(chunk)))
.withNetwork(network)
.start();

let orbitalStatus = await inspectById(orbital.getId());
while (orbitalStatus.State.Running) {
await sleep(500);
orbitalStatus = await inspectById(orbital.getId());
}

await Promise.all([pihole1.stop(), pihole2.stop(), pihole3.stop()]);
await network.stop();
expect(orbitalStatus.State.ExitCode).toBe(0);
}, 300000);
describe('Alpine', () => {
it('should sync two targets and exit with "zero" exit code', async () => {
const network = await new Network().start();
const [pihole1, pihole2, pihole3, orbitalImage] = await Promise.all([
createPiholeContainer({ password: 'primary' }).withNetwork(network).start(),
createPiholeContainer({ password: 'secondary' }).withNetwork(network).start(),
createPiholeContainer({ password: 'tertiary' }).withNetwork(network).start(),
createOrbitalSyncContainer(OrbitalBaseImage.Alpine)
]);

const orbital = await orbitalImage
.withEnvironment({
PRIMARY_HOST_BASE_URL: `http://${pihole1.getIpAddress(network.getName())}`,
PRIMARY_HOST_PASSWORD: 'primary',
SECONDARY_HOST_1_BASE_URL: `http://${pihole2.getIpAddress(network.getName())}`,
SECONDARY_HOST_1_PASSWORD: 'secondary',
SECONDARY_HOST_2_BASE_URL: `http://${pihole3.getIpAddress(network.getName())}`,
SECONDARY_HOST_2_PASSWORD: 'tertiary',
RUN_ONCE: 'true',
VERBOSE: 'true'
})
.withLogConsumer((stream) => stream.on('data', (chunk) => console.log(chunk)))
.withNetwork(network)
.start();

let orbitalStatus = await inspectById(orbital.getId());
while (orbitalStatus.State.Running) {
await sleep(500);
orbitalStatus = await inspectById(orbital.getId());
}

await Promise.all([pihole1.stop(), pihole2.stop(), pihole3.stop()]);
await network.stop();
expect(orbitalStatus.State.ExitCode).toBe(0);
}, 300000);
});

describe('Distroless', () => {
it('should sync two targets and exit with "zero" exit code', async () => {
const network = await new Network().start();
const [pihole1, pihole2, pihole3, orbitalImage] = await Promise.all([
createPiholeContainer({ password: 'primary' }).withNetwork(network).start(),
createPiholeContainer({ password: 'secondary' }).withNetwork(network).start(),
createPiholeContainer({ password: 'tertiary' }).withNetwork(network).start(),
createOrbitalSyncContainer(OrbitalBaseImage.Distroless)
]);

const orbital = await orbitalImage
.withEnvironment({
PRIMARY_HOST_BASE_URL: `http://${pihole1.getIpAddress(network.getName())}`,
PRIMARY_HOST_PASSWORD: 'primary',
SECONDARY_HOST_1_BASE_URL: `http://${pihole2.getIpAddress(network.getName())}`,
SECONDARY_HOST_1_PASSWORD: 'secondary',
SECONDARY_HOST_2_BASE_URL: `http://${pihole3.getIpAddress(network.getName())}`,
SECONDARY_HOST_2_PASSWORD: 'tertiary',
RUN_ONCE: 'true',
VERBOSE: 'true'
})
.withLogConsumer((stream) => stream.on('data', (chunk) => console.log(chunk)))
.withNetwork(network)
.start();

let orbitalStatus = await inspectById(orbital.getId());
while (orbitalStatus.State.Running) {
await sleep(500);
orbitalStatus = await inspectById(orbital.getId());
}

await Promise.all([pihole1.stop(), pihole2.stop(), pihole3.stop()]);
await network.stop();
expect(orbitalStatus.State.ExitCode).toBe(0);
}, 300000);
});
});

0 comments on commit a93c38e

Please sign in to comment.