From 21b9ea57db559460b8d00760a590f58afeed75d8 Mon Sep 17 00:00:00 2001 From: Natalie Rose Date: Mon, 7 Sep 2026 12:21:33 +1000 Subject: [PATCH] Config hardening --- .gitignore | 1 + deploy.sh | 10 ++++++++ docker-compose.yml | 61 ++++++++++++++++++++++++++++++---------------- mysql-dump.sh | 34 ++++++++++++++++++++++++++ schedule.ini | 8 ++++++ 5 files changed, 93 insertions(+), 21 deletions(-) create mode 100644 deploy.sh create mode 100755 mysql-dump.sh diff --git a/.gitignore b/.gitignore index 686f6ab..1728948 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ .env etc/lacuna.conf +data/ diff --git a/deploy.sh b/deploy.sh new file mode 100644 index 0000000..45e5435 --- /dev/null +++ b/deploy.sh @@ -0,0 +1,10 @@ +#!/usr/bin/bash + +echo '==> Pulling new images'; +sudo docker compose pull; + +echo '==> Starting new containers'; +sudo docker compose up -d --remove-orphans; + +echo '==> Cleaning up old builds'; +sudo docker image prune -f diff --git a/docker-compose.yml b/docker-compose.yml index c62db32..12995bc 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -15,17 +15,15 @@ services: beanstalk-console: container_name: beanstalk-console + restart: unless-stopped depends_on: - beanstalk image: schickling/beanstalkd-console - ports: - - 2080:2080 beanstalk: container_name: beanstalk + restart: unless-stopped image: schickling/beanstalkd - ports: - - 11300:11300 logrotate: build: @@ -39,6 +37,7 @@ services: memcached: container_name: memcached + restart: unless-stopped image: memcached command: # Persist cache in a file to save sessions (and other data) between restarts of the docker container @@ -49,13 +48,22 @@ services: mysql: container_name: mysql platform: linux/amd64 + restart: unless-stopped environment: MYSQL_ROOT_PASSWORD: ${LACUNA_MYSQL_ROOT_PASSWORD:?error} image: mysql:5.5 volumes: - ./data/mysql:/var/lib/mysql - ports: - - 3306:3306 + - ./data/backups:/backups + - ./mysql-dump.sh:/usr/local/bin/mysql-dump.sh:ro + healthcheck: + # mysqladmin ping exits 0 once mysqld accepts connections; passing creds + # makes it a truer check (it would also exit 0 on an auth failure). + test: ["CMD-SHELL", "mysqladmin ping -h 127.0.0.1 -u root -p\"$$MYSQL_ROOT_PASSWORD\" --silent"] + interval: 10s + timeout: 5s + retries: 10 + start_period: 30s caddy: image: caddy:2.8-alpine @@ -64,7 +72,7 @@ services: logging: *default-logging depends_on: server: - condition: service_started + condition: service_healthy v2-api: condition: service_started ports: @@ -84,10 +92,9 @@ services: phpmyadmin: container_name: phpmyadmin image: phpmyadmin:5.2-apache + restart: unless-stopped environment: PMA_HOST: mysql - ports: - - 8888:80 depends_on: - mysql links: @@ -99,9 +106,12 @@ services: container_name: server restart: unless-stopped depends_on: - - mysql - - memcached - - beanstalk + mysql: + condition: service_healthy + memcached: + condition: service_started + beanstalk: + condition: service_started environment: # Root of the host-mounted log tree (see the ./../../log mount below). # Scripts fall back to this same path when the var is unset. @@ -109,14 +119,20 @@ services: # Disables the prod-only size-limit / profiling middleware (bin/lacuna.psgi). TLE_NO_MIDDLEWARE: 1 logging: *default-logging - ports: - - 3050:5000 volumes: - ./etc:/home/lacuna/server/etc - ./captcha:/home/lacuna/server/captcha - ./tmp:/tmp - ./log:/home/lacuna/server/log - ./var:/home/lacuna/server/var + healthcheck: + # /starman_ping (bin/lacuna.psgi) returns "pong" only once the Lacuna app + # has compiled and Starman is serving requests. curl is in the server image. + test: ["CMD", "curl", "-fsS", "http://localhost:5000/starman_ping"] + interval: 15s + timeout: 10s + retries: 5 + start_period: 180s server-building-scheduler: image: ${CONTAINER_REGISTRY_URL:?error}/tlecommunity/server:latest @@ -124,7 +140,8 @@ services: container_name: server-building-scheduler restart: unless-stopped depends_on: - - server + server: + condition: service_healthy environment: LACUNA_LOG_DIR: /home/lacuna/server/log logging: *default-logging @@ -139,7 +156,8 @@ services: container_name: server-ship-scheduler restart: unless-stopped depends_on: - - server + server: + condition: service_healthy environment: LACUNA_LOG_DIR: /home/lacuna/server/log logging: *default-logging @@ -154,7 +172,8 @@ services: container_name: server-captcha-scheduler restart: unless-stopped depends_on: - - server + server: + condition: service_healthy environment: LACUNA_LOG_DIR: /home/lacuna/server/log logging: *default-logging @@ -168,7 +187,8 @@ services: image: mcuadros/ofelia:latest restart: unless-stopped depends_on: - - server + server: + condition: service_healthy command: daemon logging: *default-logging volumes: @@ -185,12 +205,11 @@ services: v2-api: image: ${CONTAINER_REGISTRY_URL:?error}/tlecommunity/v2-api:latest + restart: unless-stopped container_name: v2-api command: node index.ts - ports: - - 5999:80 environment: - PORT: 80 + PORT: 5999 networks: default: diff --git a/mysql-dump.sh b/mysql-dump.sh new file mode 100755 index 0000000..8071b0a --- /dev/null +++ b/mysql-dump.sh @@ -0,0 +1,34 @@ +#!/bin/sh +# Nightly logical backup of the lacuna database. +# +# Runs *inside* the mysql container, invoked on a schedule by Ofelia +# (see the [job-exec "mysql-dump"] block in schedule.ini). Output and retention +# live on the host-mounted ./data/backups tree. +# +# /bin/sh in the mysql:5.5 image is dash, which has no `set -o pipefail`, so the +# dump is written to disk first and only compressed/published once mysqldump has +# exited 0 -- a failed run never leaves a half-written .sql.gz under the real name. +set -eu + +BACKUP_DIR=/backups +KEEP_DAYS=7 +STAMP=$(date +%Y%m%d-%H%M%S) +RAW="$BACKUP_DIR/.lacuna-$STAMP.sql" +DEST="$BACKUP_DIR/lacuna-$STAMP.sql.gz" + +mkdir -p "$BACKUP_DIR" + +mysqldump -u root -p"$MYSQL_ROOT_PASSWORD" \ + --single-transaction --quick \ + --routines --triggers --events \ + --add-drop-database --databases lacuna > "$RAW" + +gzip "$RAW" # -> $RAW.gz (still hidden) +mv "$RAW.gz" "$DEST" # publish atomically under the real name + +# Retention: drop published backups older than KEEP_DAYS, plus any stale work +# files left behind by an interrupted run. +find "$BACKUP_DIR" -maxdepth 1 -name 'lacuna-*.sql.gz' -type f -mtime "+$KEEP_DAYS" -delete +find "$BACKUP_DIR" -maxdepth 1 -name '.lacuna-*.sql*' -type f -mtime +1 -delete + +echo "mysql-dump: wrote $DEST ($(du -h "$DEST" | cut -f1))" diff --git a/schedule.ini b/schedule.ini index 4e7242d..e556a8f 100644 --- a/schedule.ini +++ b/schedule.ini @@ -40,3 +40,11 @@ command = ./run_daily.sh schedule = 0 15 6 * * 0 container = server command = ./run_weekly.sh + +# Nightly logical backup of the lacuna db at 04:30. Script lives in this repo and +# is bind-mounted into the mysql container; dumps + 7-day retention land on the +# host under ./data/backups. See mysql-dump.sh. +[job-exec "mysql-dump"] +schedule = 0 30 4 * * * +container = mysql +command = /usr/local/bin/mysql-dump.sh -- 2.51.2