Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
76 changes: 76 additions & 0 deletions .github/workflows/docker-publish.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
name: Build and Publish CMS Image

# Builds the CMS backend Docker image and pushes it to GitHub Container Registry
# (GHCR). Pushes are tagged with the full commit SHA (immutable, used for
# production deploys) and, on the default branch, a moving `latest` tag.
# Pull requests build the image to catch breakage but never push.

on:
push:
branches:
- main
tags:
- "v*"
pull_request:
branches:
- main
workflow_dispatch:

# Only the latest run per ref needs to finish; cancel superseded ones.
concurrency:
group: docker-publish-${{ github.ref }}
cancel-in-progress: true

permissions:
contents: read
packages: write

jobs:
build-and-push:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4

# GHCR image names must be lowercase; the org (DrexelTriangle) is not.
- name: Compute lowercase image name
id: image
run: echo "name=ghcr.io/${GITHUB_REPOSITORY,,}" >> "$GITHUB_OUTPUT"

- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3

# Skip login on pull_request builds: fork PRs have no push rights and
# must not receive registry credentials.
- name: Log in to GHCR
if: github.event_name != 'pull_request'
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}

- name: Derive image tags and labels
id: meta
uses: docker/metadata-action@v5
with:
images: ${{ steps.image.outputs.name }}
tags: |
type=sha,format=long,prefix=
type=raw,value=latest,enable={{is_default_branch}}
type=ref,event=tag
labels: |
org.opencontainers.image.title=triangle-cms
org.opencontainers.image.source=https://github.com/${{ github.repository }}

- name: Build and push
uses: docker/build-push-action@v6
with:
context: ./server
file: ./server/Dockerfile
# Push on branch/tag builds and manual runs; PRs build only.
push: ${{ github.event_name != 'pull_request' }}
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha
cache-to: type=gha,mode=max
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,4 @@
/.env
frontend/.env
server/data/
..env.un~
4 changes: 4 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,10 @@ services:
- "8080:8080"
volumes:
- cms_activity_data:/app/data
# TLS certs are mounted at runtime (not baked into the image). Generate
# local dev certs with scripts/generate_certs.sh. In production these are
# provided by the host / replaced by Nginx TLS termination.
- ./server/certs:/app/certs:ro
networks:
- triangle_net

Expand Down
40 changes: 40 additions & 0 deletions scripts/generate_certs.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
#!/usr/bin/env bash
# Generate a self-signed TLS certificate for local development.
#
# TLS certs are intentionally NOT committed to git or baked into the Docker
# image. Each developer / server generates its own. docker-compose mounts
# ./server/certs into the CMS container at runtime.
#
# Usage:
# scripts/generate_certs.sh # generate if missing
# scripts/generate_certs.sh --force # overwrite existing certs
set -euo pipefail

repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
cert_dir="${repo_root}/server/certs"
crt="${cert_dir}/localhost.crt"
key="${cert_dir}/localhost.key"

force=false
if [[ "${1:-}" == "--force" ]]; then
force=true
fi

if [[ -f "$crt" && -f "$key" && "$force" != true ]]; then
echo "Certs already exist at ${cert_dir} (use --force to overwrite)."
exit 0
fi

mkdir -p "$cert_dir"

openssl req -x509 -newkey rsa:2048 -nodes \
-keyout "$key" \
-out "$crt" \
-days 365 \
-subj "/CN=localhost" \
-addext "subjectAltName=DNS:localhost,IP:127.0.0.1"

chmod 600 "$key"
echo "Generated self-signed dev cert:"
echo " ${crt}"
echo " ${key}"
11 changes: 11 additions & 0 deletions server/.dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# Keep secrets and machine-specific state out of the build context / image.
certs/
.env
.env.*
data/
cover.out
report.json

# Editor / VCS noise
*.un~
.git
5 changes: 4 additions & 1 deletion server/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,10 @@ FROM alpine:3.21
WORKDIR /app

COPY --from=builder /bin/cms /app/cms
COPY certs /app/certs

# TLS certificates are NOT baked into the image. They are environment-specific
# secrets and are provided at runtime (bind mount / Nginx termination). See
# docker-compose.yml and scripts/generate_certs.sh.

EXPOSE 8080
ENTRYPOINT ["/app/cms"]
19 changes: 0 additions & 19 deletions server/certs/localhost.crt

This file was deleted.

28 changes: 0 additions & 28 deletions server/certs/localhost.key

This file was deleted.

Loading