diff --git a/.gitignore b/.gitignore index 5337cab..60d3bca 100644 --- a/.gitignore +++ b/.gitignore @@ -23,6 +23,7 @@ backend/pocketbase.exe .env.local .env.*.local !.env.example +!ansible/files/.env # IDEs & System files .vscode/ diff --git a/Dockerfile b/Dockerfile index 6d09f8a..b070340 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,18 +1,19 @@ # syntax=docker/dockerfile:1 # ------------------------------------------------------------------------------ -# Stage 1: Install frontend dependencies +# Stage 1: Install frontend dependencies (Native execution on host architecture) # ------------------------------------------------------------------------------ -FROM node:lts-trixie-slim AS deps +FROM --platform=$BUILDPLATFORM node:22-bookworm-slim AS deps WORKDIR /app/frontend COPY frontend/package.json frontend/package-lock.json* ./ -RUN npm ci +RUN --mount=type=cache,target=/root/.npm \ + npm ci # ------------------------------------------------------------------------------ -# Stage 2: Build the TanStack Start frontend +# Stage 2: Build the TanStack Start frontend (Native execution on host architecture) # ------------------------------------------------------------------------------ -FROM node:lts-trixie-slim AS builder +FROM --platform=$BUILDPLATFORM node:22-bookworm-slim AS builder WORKDIR /app/frontend COPY --from=deps /app/frontend/node_modules ./node_modules @@ -22,25 +23,28 @@ ENV NODE_ENV=production RUN npm run build # ------------------------------------------------------------------------------ -# Stage 3: Fetch PocketBase binary for target architecture +# Stage 3: Fetch PocketBase binary for target architecture (Native host downloader) # ------------------------------------------------------------------------------ -FROM alpine:3.20 AS pb-downloader -ARG TARGETARCH=amd64 +FROM --platform=$BUILDPLATFORM alpine:3.20 AS pb-downloader +ARG TARGETARCH ARG PB_VERSION=0.40.2 RUN apk add --no-cache curl unzip ca-certificates \ - && PB_ARCH="${TARGETARCH}" \ - && if [ "$TARGETARCH" = "x86_64" ]; then PB_ARCH="amd64"; fi \ - && if [ "$TARGETARCH" = "aarch64" ]; then PB_ARCH="arm64"; fi \ + && case "${TARGETARCH}" in \ + amd64) PB_ARCH="amd64" ;; \ + arm64) PB_ARCH="arm64" ;; \ + arm) PB_ARCH="armv7" ;; \ + *) echo "Unsupported target architecture: ${TARGETARCH}" >&2; exit 1 ;; \ + esac \ && curl -fsSL -o /tmp/pb.zip "https://github.com/pocketbase/pocketbase/releases/download/v${PB_VERSION}/pocketbase_${PB_VERSION}_linux_${PB_ARCH}.zip" \ && unzip /tmp/pb.zip -d /app \ && chmod +x /app/pocketbase \ && rm -f /tmp/pb.zip # ------------------------------------------------------------------------------ -# Stage 4: Production runner +# Stage 4: Production runner (Target architecture) # ------------------------------------------------------------------------------ -FROM node:lts-trixie-slim AS runner +FROM node:22-bookworm-slim AS runner LABEL maintainer="Amritanshu " RUN apt-get update && apt-get install -y --no-install-recommends \ @@ -62,10 +66,10 @@ COPY docker-entrypoint.sh /app/docker-entrypoint.sh RUN chmod +x /app/docker-entrypoint.sh # Environment variables -ENV NODE_ENV=production -ENV PORT=3000 -ENV HOST=0.0.0.0 -ENV PB_DATA_DIR=/app/pb_data +ENV NODE_ENV=production \ + PORT=3000 \ + HOST=0.0.0.0 \ + PB_DATA_DIR=/app/pb_data # Persistent data volume for PocketBase SQLite database and file storage VOLUME ["/app/pb_data"] @@ -74,4 +78,8 @@ VOLUME ["/app/pb_data"] # Port 8090: PocketBase REST API & Admin UI EXPOSE 3000 8090 +# Health check for both PocketBase and frontend server +HEALTHCHECK --interval=15s --timeout=3s --start-period=5s --retries=3 \ + CMD curl -fsS http://127.0.0.1:8090/api/health > /dev/null && curl -fsS http://127.0.0.1:3000 > /dev/null || exit 1 + ENTRYPOINT ["/app/docker-entrypoint.sh"] diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..19b772b --- /dev/null +++ b/Makefile @@ -0,0 +1,36 @@ +.PHONY: build-production +build-production: ## Build the production docker image and push to private registry + @docker buildx build \ + --platform linux/amd64,linux/arm64 \ + --tag registry.tanshu.com/hops:latest \ + $(if $(filter-out latest,$(TAG)),--tag registry.tanshu.com/hops:$(TAG)) \ + --push \ + --pull \ + git@git.tanshu.com:tanshu/hops.git + +.PHONY: build-check +build-check: ## Multi-arch build without push (compile check) + @docker buildx build \ + --platform linux/amd64,linux/arm64 \ + --tag hops:test \ + --pull \ + --progress=plain \ + git@git.tanshu.com:tanshu/hops.git + +.PHONY: build-check-local +build-check-local: ## Multi-arch build without push (compile check) + @git archive --format=tar HEAD | docker buildx build \ + --platform linux/amd64 \ + --tag hops:test \ + --pull \ + --progress=plain \ + --load \ + - + +# .PHONY: deploy +# deploy: ## Build, upload to registry, and trigger Ansible deployment +# @./deploy.sh --deploy $(if $(TAG),$(TAG)) + +# .PHONY: ansible-deploy +# ansible-deploy: ## Run Ansible playbook directly +# @cd ansible && ansible-playbook -i hosts playbook.yml $(if $(TAG),-e "tag=$(TAG)") diff --git a/ansible/files/.env b/ansible/files/.env new file mode 100644 index 0000000..c83332d --- /dev/null +++ b/ansible/files/.env @@ -0,0 +1,6 @@ +NODE_ENV=production +PORT=3000 +HOST=0.0.0.0 +PB_DATA_DIR=/app/pb_data +PB_ADMIN_EMAIL={{ pb_admin_email }} +PB_ADMIN_PASSWORD={{ pb_admin_password }} diff --git a/ansible/files/Caddyfile.j2 b/ansible/files/Caddyfile.j2 new file mode 100644 index 0000000..e01c5dc --- /dev/null +++ b/ansible/files/Caddyfile.j2 @@ -0,0 +1,20 @@ +{{ host }}{% if apex_host is defined and apex_host %}, {{ apex_host }}{% endif %} { + # PocketBase Admin UI and REST API + handle /_/* { + reverse_proxy {{ container_name }}:8090 + } + handle /api/* { + reverse_proxy {{ container_name }}:8090 + } + + # TanStack Start Frontend Application + handle { + reverse_proxy {{ container_name }}:3000 + } +} + +{% if admin_host is defined and admin_host %} +{{ admin_host }} { + reverse_proxy {{ container_name }}:8090 +} +{% endif %} diff --git a/ansible/playbook.yml b/ansible/playbook.yml new file mode 100644 index 0000000..1298842 --- /dev/null +++ b/ansible/playbook.yml @@ -0,0 +1,11 @@ +--- +- name: Deploy Hops n Grains Microbrewery + hosts: monoco + become: true + vars_files: + - vars/default.yml + + roles: + - network + - hops + - caddy diff --git a/ansible/requirements.yml b/ansible/requirements.yml new file mode 100644 index 0000000..660f775 --- /dev/null +++ b/ansible/requirements.yml @@ -0,0 +1,3 @@ +--- +collections: + - name: community.docker diff --git a/ansible/roles/caddy/defaults/main.yaml b/ansible/roles/caddy/defaults/main.yaml new file mode 100644 index 0000000..259ebf2 --- /dev/null +++ b/ansible/roles/caddy/defaults/main.yaml @@ -0,0 +1,3 @@ +--- +caddy_container: caddy +caddy_caddyfile_path: /var/lib/caddy/conf/Caddyfile diff --git a/ansible/roles/caddy/handlers/main.yaml b/ansible/roles/caddy/handlers/main.yaml new file mode 100644 index 0000000..5fd9c8a --- /dev/null +++ b/ansible/roles/caddy/handlers/main.yaml @@ -0,0 +1,5 @@ +--- +- name: Reload Caddy configuration + ansible.builtin.command: "docker exec -w /etc/caddy {{ caddy_container }} caddy reload" + listen: "Reload Caddy" + changed_when: true diff --git a/ansible/roles/caddy/tasks/main.yaml b/ansible/roles/caddy/tasks/main.yaml new file mode 100644 index 0000000..3fdaee9 --- /dev/null +++ b/ansible/roles/caddy/tasks/main.yaml @@ -0,0 +1,26 @@ +--- +- name: Read snippet from template file + ansible.builtin.set_fact: + caddy_snippet_block: "{{ lookup('template', 'files/Caddyfile.j2') }}" + +- name: Read current Caddyfile + ansible.builtin.slurp: + path: "{{ caddy_caddyfile_path }}" + register: caddy_caddyfile_raw + +- name: Decode Caddyfile content + ansible.builtin.set_fact: + caddy_caddyfile_content: "{{ caddy_caddyfile_raw['content'] | b64decode }}" + +- name: Check if snippet already exists + ansible.builtin.set_fact: + caddy_snippet_present: "{{ caddy_snippet_block in caddy_caddyfile_content }}" + +- name: Add or update snippet in Caddyfile + ansible.builtin.blockinfile: + path: "{{ caddy_caddyfile_path }}" + marker: "# {mark} Ansible managed Caddy snippet for {{ host }}" + block: "{{ caddy_snippet_block }}" + create: true + mode: "0644" + notify: Reload Caddy configuration diff --git a/ansible/roles/hops/tasks/main.yaml b/ansible/roles/hops/tasks/main.yaml new file mode 100644 index 0000000..b5e9dd0 --- /dev/null +++ b/ansible/roles/hops/tasks/main.yaml @@ -0,0 +1,43 @@ +--- +- name: Log in to private Docker registry + community.docker.docker_login: + registry: "{{ registry }}" + username: "{{ username }}" + password: "{{ password }}" + +- name: Pull Hops n Grains image + community.docker.docker_image: + name: "{{ image_name }}" + source: pull + state: present + force_source: true + +- name: Ensure Host Directory exists + ansible.builtin.file: + path: "{{ host_data_path }}" + state: directory + mode: "0755" + +- name: Ensure PocketBase data directory exists + ansible.builtin.file: + path: "{{ host_data_path }}/pb_data" + state: directory + mode: "0755" + +- name: Upload the .env file + ansible.builtin.template: + src: "files/.env" + dest: "{{ host_data_path }}/.env" + mode: "0600" + +- name: Create and run Hops n Grains container + community.docker.docker_container: + name: "{{ container_name }}" + image: "{{ image_name }}" + state: started + restart_policy: "unless-stopped" + env_file: "{{ host_data_path }}/.env" + volumes: + - "{{ host_data_path }}/pb_data:/app/pb_data" + networks: + - name: "{{ docker_network }}" diff --git a/ansible/roles/network/tasks/main.yaml b/ansible/roles/network/tasks/main.yaml new file mode 100644 index 0000000..f8d7781 --- /dev/null +++ b/ansible/roles/network/tasks/main.yaml @@ -0,0 +1,8 @@ +--- +- name: Ensure Docker network exists + community.docker.docker_network: + name: "{{ docker_network }}" + state: present + connected: + - "{{ caddy_container }}" + appends: true diff --git a/ansible/vars/default.yml b/ansible/vars/default.yml new file mode 100644 index 0000000..f630c34 --- /dev/null +++ b/ansible/vars/default.yml @@ -0,0 +1,28 @@ +--- +registry: registry.tanshu.com +username: ta-registry +password: ff28a01f00c0f39315d94cd9dcb1e554968dba25676a8ea5f2be34e96a9a099f + +title: hops +tag: latest + +image_name: "{{ registry }}/hops:{{ tag }}" + +app_name: hops +host_directory: "{{ app_name }}" +host_data_path: "/var/lib/{{ host_directory }}" +container_name: "{{ app_name }}" +docker_network: "{{ app_name }}_net" + +# Public host for frontend & PocketBase routing +host: www.hopsngrains.com +apex_host: hopsngrains.com +admin_host: admin.hopsngrains.com + +# Caddy configuration +caddy_container: caddy +caddy_caddyfile_path: /var/lib/caddy/conf/Caddyfile + +# PocketBase Initial Superuser Credentials +pb_admin_email: admin@hopsngrains.com +pb_admin_password: HopsPassword1234! diff --git a/deploy.sh b/deploy.sh new file mode 100755 index 0000000..9c70ad1 --- /dev/null +++ b/deploy.sh @@ -0,0 +1,19 @@ +#!/usr/bin/env bash +set -euo pipefail + +parent_path=$( cd "$(dirname "${BASH_SOURCE[0]}")" || exit ; pwd -P ) +cd "$parent_path" || exit + +echo "========================================================" +echo " Building & Uploading Docker Image" +echo "========================================================" +current_version="${1:-latest}" +make build-production TAG="$current_version" + + +echo "" +echo "========================================================" +echo " Executing Ansible Deployment" +echo "========================================================" +cd "$parent_path/ansible" || exit +ansible-playbook playbook.yml -e "tag=$current_version" diff --git a/docker-entrypoint.sh b/docker-entrypoint.sh index cf4a902..ae618e8 100755 --- a/docker-entrypoint.sh +++ b/docker-entrypoint.sh @@ -1,4 +1,4 @@ -#!/bin/sh +#!/bin/bash set -e # Data directory for PocketBase